From 4a3f4d4b0690acc0cd07856c817e1afbe34c0cc6 Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Wed, 17 Jun 2026 11:30:11 +0530 Subject: [PATCH] Initial project setup --- .cursor/rules/erp-backend-core.mdc | 39 + .cursor/rules/erp-backend-modules.mdc | 43 + .cursor/rules/erp-backend-prisma.mdc | 41 + .env.example | 42 + .eslintrc.json | 9 + .gitignore | 6 + .prettierrc | 6 + BACKEND_SETUP.md | 1518 ++++ BACKEND_TASKS.md | 186 + SETUP_STATUS.md | 32 + jest.config.js | 5 + nodemon.json | 4 + package-lock.json | 7737 +++++++++++++++++ package.json | 60 + prisma/schema.prisma | 891 ++ prisma/seed.js | 49 + scripts/setup-db.sh | 39 + src/app.js | 55 + src/config/env.js | 45 + src/config/logger.js | 53 + src/config/morgan.js | 12 + src/config/prisma.js | 21 + src/config/swagger.js | 45 + src/docs/completed-routes.yaml | 1641 ++++ src/middlewares/auth.middleware.js | 39 + src/middlewares/error.middleware.js | 31 + src/middlewares/rateLimiter.middleware.js | 21 + src/middlewares/rbac.middleware.js | 13 + src/middlewares/requestId.middleware.js | 7 + src/middlewares/upload.middleware.js | 28 + src/middlewares/validate.middleware.js | 20 + src/modules/auth/auth.controller.js | 20 + src/modules/auth/auth.routes.js | 13 + src/modules/auth/auth.service.js | 94 + src/modules/auth/auth.validation.js | 12 + src/modules/masters/_shared/master.factory.js | 148 + .../asset-categories.controller.js | 30 + .../asset-categories.routes.js | 17 + .../asset-categories.service.js | 68 + .../asset-categories.validation.js | 21 + .../masters/brands/brands.controller.js | 30 + src/modules/masters/brands/brands.routes.js | 17 + src/modules/masters/brands/brands.service.js | 74 + .../masters/brands/brands.validation.js | 22 + .../delivery-terms.controller.js | 30 + .../delivery-terms/delivery-terms.routes.js | 17 + .../delivery-terms/delivery-terms.service.js | 56 + .../delivery-terms.validation.js | 19 + .../departments/departments.controller.js | 30 + .../masters/departments/departments.routes.js | 17 + .../departments/departments.service.js | 44 + .../departments/departments.validation.js | 17 + .../designations/designations.controller.js | 30 + .../designations/designations.routes.js | 17 + .../designations/designations.service.js | 44 + .../designations/designations.validation.js | 17 + .../document-series.controller.js | 30 + .../document-series/document-series.routes.js | 17 + .../document-series.service.js | 68 + .../document-series.validation.js | 21 + .../masters/gst-rates/gst-rates.controller.js | 30 + .../masters/gst-rates/gst-rates.routes.js | 17 + .../masters/gst-rates/gst-rates.service.js | 38 + .../masters/gst-rates/gst-rates.validation.js | 18 + src/modules/masters/index.js | 20 + .../item-categories.controller.js | 30 + .../item-categories/item-categories.routes.js | 17 + .../item-categories.service.js | 50 + .../item-categories.validation.js | 18 + .../item-subcategories.controller.js | 30 + .../item-subcategories.routes.js | 17 + .../item-subcategories.service.js | 56 + .../item-subcategories.validation.js | 19 + src/modules/masters/items/items.controller.js | 30 + src/modules/masters/items/items.routes.js | 17 + src/modules/masters/items/items.service.js | 116 + src/modules/masters/items/items.validation.js | 29 + .../payment-terms/payment-terms.controller.js | 30 + .../payment-terms/payment-terms.routes.js | 17 + .../payment-terms/payment-terms.service.js | 62 + .../payment-terms/payment-terms.validation.js | 20 + .../masters/plants/plants.controller.js | 30 + src/modules/masters/plants/plants.routes.js | 17 + src/modules/masters/plants/plants.service.js | 86 + .../masters/plants/plants.validation.js | 24 + src/modules/masters/uom/uom.controller.js | 30 + src/modules/masters/uom/uom.routes.js | 23 + src/modules/masters/uom/uom.service.js | 104 + src/modules/masters/uom/uom.validation.js | 22 + .../warehouses/warehouses.controller.js | 30 + .../masters/warehouses/warehouses.routes.js | 17 + .../masters/warehouses/warehouses.service.js | 62 + .../warehouses/warehouses.validation.js | 20 + src/modules/roles/roles.controller.js | 76 + src/modules/roles/roles.routes.js | 43 + src/modules/roles/roles.service.js | 355 + src/modules/roles/roles.validation.js | 51 + src/modules/users/users.controller.js | 47 + src/modules/users/users.routes.js | 35 + src/modules/users/users.service.js | 363 + src/modules/users/users.validation.js | 53 + src/routes/v1/index.js | 14 + src/server.js | 46 + src/utils/ApiError.js | 11 + src/utils/ApiResponse.js | 10 + src/utils/asyncHandler.js | 1 + src/utils/auditLog.js | 15 + src/utils/encryption.js | 35 + src/utils/generateCode.js | 19 + src/utils/pagination.js | 8 + 110 files changed, 16106 insertions(+) create mode 100644 .cursor/rules/erp-backend-core.mdc create mode 100644 .cursor/rules/erp-backend-modules.mdc create mode 100644 .cursor/rules/erp-backend-prisma.mdc create mode 100644 .env.example create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 BACKEND_SETUP.md create mode 100644 BACKEND_TASKS.md create mode 100644 SETUP_STATUS.md create mode 100644 jest.config.js create mode 100644 nodemon.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 prisma/schema.prisma create mode 100644 prisma/seed.js create mode 100755 scripts/setup-db.sh create mode 100644 src/app.js create mode 100644 src/config/env.js create mode 100644 src/config/logger.js create mode 100644 src/config/morgan.js create mode 100644 src/config/prisma.js create mode 100644 src/config/swagger.js create mode 100644 src/docs/completed-routes.yaml create mode 100644 src/middlewares/auth.middleware.js create mode 100644 src/middlewares/error.middleware.js create mode 100644 src/middlewares/rateLimiter.middleware.js create mode 100644 src/middlewares/rbac.middleware.js create mode 100644 src/middlewares/requestId.middleware.js create mode 100644 src/middlewares/upload.middleware.js create mode 100644 src/middlewares/validate.middleware.js create mode 100644 src/modules/auth/auth.controller.js create mode 100644 src/modules/auth/auth.routes.js create mode 100644 src/modules/auth/auth.service.js create mode 100644 src/modules/auth/auth.validation.js create mode 100644 src/modules/masters/_shared/master.factory.js create mode 100644 src/modules/masters/asset-categories/asset-categories.controller.js create mode 100644 src/modules/masters/asset-categories/asset-categories.routes.js create mode 100644 src/modules/masters/asset-categories/asset-categories.service.js create mode 100644 src/modules/masters/asset-categories/asset-categories.validation.js create mode 100644 src/modules/masters/brands/brands.controller.js create mode 100644 src/modules/masters/brands/brands.routes.js create mode 100644 src/modules/masters/brands/brands.service.js create mode 100644 src/modules/masters/brands/brands.validation.js create mode 100644 src/modules/masters/delivery-terms/delivery-terms.controller.js create mode 100644 src/modules/masters/delivery-terms/delivery-terms.routes.js create mode 100644 src/modules/masters/delivery-terms/delivery-terms.service.js create mode 100644 src/modules/masters/delivery-terms/delivery-terms.validation.js create mode 100644 src/modules/masters/departments/departments.controller.js create mode 100644 src/modules/masters/departments/departments.routes.js create mode 100644 src/modules/masters/departments/departments.service.js create mode 100644 src/modules/masters/departments/departments.validation.js create mode 100644 src/modules/masters/designations/designations.controller.js create mode 100644 src/modules/masters/designations/designations.routes.js create mode 100644 src/modules/masters/designations/designations.service.js create mode 100644 src/modules/masters/designations/designations.validation.js create mode 100644 src/modules/masters/document-series/document-series.controller.js create mode 100644 src/modules/masters/document-series/document-series.routes.js create mode 100644 src/modules/masters/document-series/document-series.service.js create mode 100644 src/modules/masters/document-series/document-series.validation.js create mode 100644 src/modules/masters/gst-rates/gst-rates.controller.js create mode 100644 src/modules/masters/gst-rates/gst-rates.routes.js create mode 100644 src/modules/masters/gst-rates/gst-rates.service.js create mode 100644 src/modules/masters/gst-rates/gst-rates.validation.js create mode 100644 src/modules/masters/index.js create mode 100644 src/modules/masters/item-categories/item-categories.controller.js create mode 100644 src/modules/masters/item-categories/item-categories.routes.js create mode 100644 src/modules/masters/item-categories/item-categories.service.js create mode 100644 src/modules/masters/item-categories/item-categories.validation.js create mode 100644 src/modules/masters/item-subcategories/item-subcategories.controller.js create mode 100644 src/modules/masters/item-subcategories/item-subcategories.routes.js create mode 100644 src/modules/masters/item-subcategories/item-subcategories.service.js create mode 100644 src/modules/masters/item-subcategories/item-subcategories.validation.js create mode 100644 src/modules/masters/items/items.controller.js create mode 100644 src/modules/masters/items/items.routes.js create mode 100644 src/modules/masters/items/items.service.js create mode 100644 src/modules/masters/items/items.validation.js create mode 100644 src/modules/masters/payment-terms/payment-terms.controller.js create mode 100644 src/modules/masters/payment-terms/payment-terms.routes.js create mode 100644 src/modules/masters/payment-terms/payment-terms.service.js create mode 100644 src/modules/masters/payment-terms/payment-terms.validation.js create mode 100644 src/modules/masters/plants/plants.controller.js create mode 100644 src/modules/masters/plants/plants.routes.js create mode 100644 src/modules/masters/plants/plants.service.js create mode 100644 src/modules/masters/plants/plants.validation.js create mode 100644 src/modules/masters/uom/uom.controller.js create mode 100644 src/modules/masters/uom/uom.routes.js create mode 100644 src/modules/masters/uom/uom.service.js create mode 100644 src/modules/masters/uom/uom.validation.js create mode 100644 src/modules/masters/warehouses/warehouses.controller.js create mode 100644 src/modules/masters/warehouses/warehouses.routes.js create mode 100644 src/modules/masters/warehouses/warehouses.service.js create mode 100644 src/modules/masters/warehouses/warehouses.validation.js create mode 100644 src/modules/roles/roles.controller.js create mode 100644 src/modules/roles/roles.routes.js create mode 100644 src/modules/roles/roles.service.js create mode 100644 src/modules/roles/roles.validation.js create mode 100644 src/modules/users/users.controller.js create mode 100644 src/modules/users/users.routes.js create mode 100644 src/modules/users/users.service.js create mode 100644 src/modules/users/users.validation.js create mode 100644 src/routes/v1/index.js create mode 100644 src/server.js create mode 100644 src/utils/ApiError.js create mode 100644 src/utils/ApiResponse.js create mode 100644 src/utils/asyncHandler.js create mode 100644 src/utils/auditLog.js create mode 100644 src/utils/encryption.js create mode 100644 src/utils/generateCode.js create mode 100644 src/utils/pagination.js diff --git a/.cursor/rules/erp-backend-core.mdc b/.cursor/rules/erp-backend-core.mdc new file mode 100644 index 0000000..1634fdf --- /dev/null +++ b/.cursor/rules/erp-backend-core.mdc @@ -0,0 +1,39 @@ +--- +description: ERP backend core stack, security, and API conventions +alwaysApply: true +--- + +# ERP Backend — Core Conventions + +Stack: Node.js 20+ · Express (CommonJS) · PostgreSQL 15+ · Prisma · Joi · Winston + +## Non-negotiables + +- CommonJS only (`require` / `module.exports`) — no ESM +- Validate all env vars on boot via `src/config/env.js`; refuse to start if invalid +- Never commit `.env`; only `.env.example` +- All protected routes: `authenticate` → `authorize(module, action)` → `validate(schema)` → controller +- Soft delete: filter `deleted_at: null`; set `deleted_at = now()` instead of hard DELETE +- Every mutation calls `auditLog({ tableName, recordId, action, oldValue, newValue, userId, requestId })` +- Controllers return `ApiResponse`; throw `ApiError` for failures — no Prisma or business logic in controllers +- Serialize `BigInt` as strings in JSON (`BigInt.prototype.toJSON`) + +## Response envelope + +```json +{ "success": true, "message": "...", "data": {}, "meta": { "page": 1, "limit": 20, "total": 57 } } +``` + +## Security + +- JWT access (15m) in Bearer header; refresh tokens hashed (SHA-256) in DB, rotated on refresh +- bcrypt passwords; account lockout after failed attempts +- AES-256-GCM + HMAC blind index for PII (mobile, bank accounts) — encrypt in service layer +- Rate limit `/api` globally; stricter limit on auth routes +- File uploads: MIME allow-list, random filenames, serve via authenticated RBAC endpoint only + +## API + +- Routes under `/api/v1/`; pagination `?page=1&limit=20` (max 100); dates ISO 8601 UTC +- RBAC actions: `view`, `create`, `edit`, `delete`, `approve`, `export` +- Reference: `BACKEND_SETUP.md`, task checklist: `BACKEND_TASKS.md` diff --git a/.cursor/rules/erp-backend-modules.mdc b/.cursor/rules/erp-backend-modules.mdc new file mode 100644 index 0000000..5e9ad07 --- /dev/null +++ b/.cursor/rules/erp-backend-modules.mdc @@ -0,0 +1,43 @@ +--- +description: HMVC module pattern for ERP backend src/modules +globs: src/modules/**/*.js +alwaysApply: false +--- + +# HMVC Module Pattern + +Each module is self-contained with this file set: + +| File | Role | +|------|------| +| `*.routes.js` | HTTP verbs + middleware chain only | +| `*.controller.js` | Parse request, call service, shape `ApiResponse` via `asyncHandler` | +| `*.service.js` | Business logic, Prisma calls, `auditLog` | +| `*.validation.js` | Joi schemas (body default; `'query'` for list filters) | +| `*.repository.js` | **Complex modules only** (PO, GRN, Assets) — multi-table `prisma.$transaction` | + +## Route middleware order + +```js +router.post('/', authenticate, authorize('VENDOR', 'create'), validate(createSchema), controller.create); +``` + +## Service rules + +- Simple masters/CRUD: call Prisma directly from service +- Auto codes: `nextDocumentNumber('VENDOR')` inside a transaction +- Pass `req.user.id` and `req.id` (request ID) into service for audit trails +- Filter all reads with `deleted_at: null` + +## Masters + +- RBAC module code: `MASTERS` +- UOM is the template — replicate four-file pattern for other masters +- Aggregate sub-routers in `src/modules/masters/index.js` + +## Do not + +- Put Prisma calls or business logic in controllers +- Skip `authorize()` on protected endpoints +- Hard-delete transactional records +- Return raw refresh tokens or passwords in API responses diff --git a/.cursor/rules/erp-backend-prisma.mdc b/.cursor/rules/erp-backend-prisma.mdc new file mode 100644 index 0000000..08bddc4 --- /dev/null +++ b/.cursor/rules/erp-backend-prisma.mdc @@ -0,0 +1,41 @@ +--- +description: Prisma schema and database conventions for ERP backend +globs: prisma/**/* +alwaysApply: false +--- + +# Prisma & Database Conventions + +## Naming + +- Models: `PascalCase` singular (e.g. `Vendor`) +- Fields: `snake_case` (match requirements doc — no camelCase mapping) +- Tables: `snake_case` plural via `@@map("vendors")` + +## Required columns (every business table) + +``` +is_active, created_by, updated_by, created_at, updated_at, deleted_at +``` + +## Patterns + +- Primary keys: `BigInt @id @default(autoincrement())` +- Soft delete everywhere on transactional tables +- Document numbering via `document_series` + atomic increment in `generateCode.js` +- Seed idempotently with `upsert` — modules, permissions, roles, bootstrap Super Admin + +## Migration workflow + +```bash +npx prisma migrate dev --name +npx prisma generate +node prisma/seed.js +``` + +## When adding a new table + +1. Add model with common columns + `@@map` +2. Create migration +3. Update seed if new module/permissions needed +4. Add service with `deleted_at: null` filters and audit logging diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1702da3 --- /dev/null +++ b/.env.example @@ -0,0 +1,42 @@ +NODE_ENV=development +PORT=3000 + +# PostgreSQL +DB_HOST=demo.venbait.in +DB_PORT=5432 +DB_NAME=bharaterp +DB_USER=bharaterpdevdbuser +DB_PASSWORD=devdbuser@bharaterp +DATABASE_URL=postgresql://bharaterpdevdbuser:devdbuser%40bharaterp@demo.venbait.in:5432/bharaterp?schema=public + +# JWT +JWT_ACCESS_SECRET=replace_with_strong_random_value_min_32_chars +JWT_ACCESS_EXPIRY=15m +JWT_REFRESH_SECRET=replace_with_another_strong_random_value_min_32_chars +JWT_REFRESH_EXPIRY=7d + +# Password hashing +BCRYPT_SALT_ROUNDS=12 + +# Encryption +ENCRYPTION_KEY=replace_with_64_char_hex_string +ENCRYPTION_HMAC_KEY=replace_with_strong_hmac_key + +# CORS +CORS_ORIGINS=http://localhost:3000,http://localhost:8080 + +# Rate limiting +RATE_LIMIT_WINDOW_MS=900000 +RATE_LIMIT_MAX=100 +AUTH_RATE_LIMIT_MAX=10 + +# Logging +LOG_LEVEL=info + +# Uploads +UPLOAD_DIR=uploads +MAX_FILE_SIZE_MB=5 + +# Account lockout +MAX_LOGIN_ATTEMPTS=5 +LOCKOUT_DURATION_MINUTES=30 diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..4b19e8f --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,9 @@ +{ + "env": { "node": true, "es2022": true, "jest": true }, + "extends": ["eslint:recommended", "plugin:prettier/recommended"], + "parserOptions": { "ecmaVersion": 2022, "sourceType": "script" }, + "rules": { + "no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }], + "no-console": "warn" + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cd34724 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +.env +logs/ +uploads/ +coverage/ +.DS_Store diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..cbd1fe3 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": true, + "singleQuote": true, + "trailingComma": "es5", + "printWidth": 100 +} diff --git a/BACKEND_SETUP.md b/BACKEND_SETUP.md new file mode 100644 index 0000000..b8d3de0 --- /dev/null +++ b/BACKEND_SETUP.md @@ -0,0 +1,1518 @@ +# ERP Backend Setup Specification (Phase 1) + +**Stack**: Node.js (LTS 20+) · Express.js (JavaScript, CommonJS) · PostgreSQL 15+ · Prisma ORM +**Purpose**: This document is the build spec for the backend scaffold — security, logging, error handling, RBAC, and the HMVC-style modular folder structure. Build it in the order given in Section 19. + +--- + +## 1. Tech Stack Summary + +| Concern | Choice | +|---|---| +| Runtime | Node.js 20 LTS | +| Framework | Express.js | +| Language | JavaScript (CommonJS — `require`/`module.exports`) | +| Database | PostgreSQL 15+ | +| ORM | Prisma | +| Auth | JWT (short-lived access + rotating refresh tokens) | +| Password hashing | bcrypt | +| Validation | Joi | +| Logging | Winston + winston-daily-rotate-file + Morgan | +| File uploads | Multer | +| API docs | swagger-jsdoc + swagger-ui-express | +| Testing | Jest + Supertest | +| Lint/format | ESLint + Prettier + Husky + lint-staged | +| Containerization | Docker + docker-compose | + +--- + +## 2. Folder Structure (HMVC pattern) + +Each business module is self-contained: routes → controller → service → (repository, for complex modules) → validation. Simple masters call Prisma directly from the service; complex transactional modules (Purchase Orders, GRN, Assets) split out a repository layer for multi-table writes/transactions. + +``` +erp-backend/ +├── prisma/ +│ ├── schema.prisma +│ ├── migrations/ +│ └── seed.js +├── src/ +│ ├── config/ +│ │ ├── env.js +│ │ ├── logger.js +│ │ ├── morgan.js +│ │ ├── prisma.js +│ │ └── swagger.js +│ ├── middlewares/ +│ │ ├── auth.middleware.js +│ │ ├── rbac.middleware.js +│ │ ├── validate.middleware.js +│ │ ├── rateLimiter.middleware.js +│ │ ├── upload.middleware.js +│ │ ├── requestId.middleware.js +│ │ └── error.middleware.js +│ ├── utils/ +│ │ ├── ApiError.js +│ │ ├── ApiResponse.js +│ │ ├── asyncHandler.js +│ │ ├── encryption.js +│ │ ├── auditLog.js +│ │ ├── pagination.js +│ │ └── generateCode.js +│ ├── modules/ +│ │ ├── auth/ +│ │ │ ├── auth.routes.js +│ │ │ ├── auth.controller.js +│ │ │ ├── auth.service.js +│ │ │ └── auth.validation.js +│ │ ├── users/ +│ │ ├── roles/ +│ │ ├── masters/ +│ │ │ ├── index.js +│ │ │ ├── uom/ +│ │ │ ├── item-categories/ +│ │ │ ├── item-subcategories/ +│ │ │ ├── items/ +│ │ │ ├── brands/ +│ │ │ ├── gst-rates/ +│ │ │ ├── warehouses/ +│ │ │ ├── payment-terms/ +│ │ │ ├── delivery-terms/ +│ │ │ ├── asset-categories/ +│ │ │ ├── departments/ +│ │ │ ├── designations/ +│ │ │ ├── plants/ +│ │ │ └── document-series/ +│ │ ├── vendors/ +│ │ ├── purchase-orders/ +│ │ ├── grn/ +│ │ └── assets/ +│ ├── routes/ +│ │ └── v1/ +│ │ └── index.js +│ ├── app.js +│ └── server.js +├── uploads/ +├── logs/ +├── tests/ +│ └── modules/ +├── .env.example +├── .eslintrc.json +├── .prettierrc +├── Dockerfile +├── docker-compose.yml +└── package.json +``` + +--- + +## 3. Environment Variables (`.env.example`) + +```env +NODE_ENV=development +PORT=3000 + +# Database +DATABASE_URL=postgresql://erp_user:erp_password@localhost:5432/erp_db?schema=public + +# JWT +JWT_ACCESS_SECRET=replace_with_strong_random_value +JWT_ACCESS_EXPIRY=15m +JWT_REFRESH_SECRET=replace_with_another_strong_random_value +JWT_REFRESH_EXPIRY=7d + +# Password hashing +BCRYPT_SALT_ROUNDS=12 + +# Field-level encryption (AES-256-GCM) - generate with: openssl rand -hex 32 +ENCRYPTION_KEY=replace_with_64_char_hex_string +# HMAC key for blind-index (searchable encrypted fields) +ENCRYPTION_HMAC_KEY=replace_with_strong_random_value + +# CORS - comma separated list of allowed origins +CORS_ORIGINS=http://localhost:3000,http://localhost:8080 + +# Rate limiting +RATE_LIMIT_WINDOW_MS=900000 +RATE_LIMIT_MAX=100 +AUTH_RATE_LIMIT_MAX=10 + +# Logging +LOG_LEVEL=info + +# File uploads +UPLOAD_DIR=uploads +MAX_FILE_SIZE_MB=5 + +# Account lockout +MAX_LOGIN_ATTEMPTS=5 +LOCKOUT_DURATION_MINUTES=30 +``` + +All variables are validated on startup (Section 5.1) — the app must refuse to boot if a required variable is missing or malformed. Never commit `.env`; only `.env.example`. + +--- + +## 4. Core Config Files + +### 4.1 `src/config/env.js` — validated environment + +```js +const Joi = require('joi'); + +const envSchema = Joi.object({ + NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'), + PORT: Joi.number().default(3000), + + DATABASE_URL: Joi.string().required(), + + JWT_ACCESS_SECRET: Joi.string().min(32).required(), + JWT_ACCESS_EXPIRY: Joi.string().default('15m'), + JWT_REFRESH_SECRET: Joi.string().min(32).required(), + JWT_REFRESH_EXPIRY: Joi.string().default('7d'), + + BCRYPT_SALT_ROUNDS: Joi.number().default(12), + + ENCRYPTION_KEY: Joi.string().length(64).required(), + ENCRYPTION_HMAC_KEY: Joi.string().min(32).required(), + + CORS_ORIGINS: Joi.string().default('*'), + + RATE_LIMIT_WINDOW_MS: Joi.number().default(900000), + RATE_LIMIT_MAX: Joi.number().default(100), + AUTH_RATE_LIMIT_MAX: Joi.number().default(10), + + LOG_LEVEL: Joi.string().default('info'), + + UPLOAD_DIR: Joi.string().default('uploads'), + MAX_FILE_SIZE_MB: Joi.number().default(5), + + MAX_LOGIN_ATTEMPTS: Joi.number().default(5), + LOCKOUT_DURATION_MINUTES: Joi.number().default(30), +}).unknown(); + +const { error, value: env } = envSchema.validate(process.env); +if (error) { + throw new Error(`Environment validation error: ${error.message}`); +} + +module.exports = env; +``` + +### 4.2 `src/config/logger.js` — Winston logger + +```js +const winston = require('winston'); +require('winston-daily-rotate-file'); +const path = require('path'); +const env = require('./env'); + +const { combine, timestamp, printf, errors, json, colorize } = winston.format; + +const SENSITIVE_KEYS = ['password', 'token', 'authorization', 'refresh_token', 'access_token']; + +const redact = winston.format((info) => { + const scrub = (obj) => { + if (!obj || typeof obj !== 'object') return obj; + const out = Array.isArray(obj) ? [] : {}; + for (const [k, v] of Object.entries(obj)) { + out[k] = SENSITIVE_KEYS.includes(k.toLowerCase()) ? '[REDACTED]' : scrub(v); + } + return out; + }; + return scrub(info); +}); + +const devFormat = combine( + redact(), + colorize(), + timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + errors({ stack: true }), + printf(({ level, message, timestamp, stack, ...meta }) => { + const metaStr = Object.keys(meta).length ? JSON.stringify(meta) : ''; + return `${timestamp} [${level}]: ${stack || message} ${metaStr}`; + }) +); + +const prodFormat = combine(redact(), timestamp(), errors({ stack: true }), json()); + +const logger = winston.createLogger({ + level: env.LOG_LEVEL, + format: env.NODE_ENV === 'production' ? prodFormat : devFormat, + transports: [ + new winston.transports.Console(), + new winston.transports.DailyRotateFile({ + filename: path.join('logs', 'error-%DATE%.log'), + datePattern: 'YYYY-MM-DD', + level: 'error', + maxFiles: '30d', + }), + new winston.transports.DailyRotateFile({ + filename: path.join('logs', 'combined-%DATE%.log'), + datePattern: 'YYYY-MM-DD', + maxFiles: '30d', + }), + ], + exitOnError: false, +}); + +module.exports = logger; +``` + +### 4.3 `src/config/morgan.js` — HTTP request logging into Winston + +```js +const morgan = require('morgan'); +const logger = require('./logger'); + +morgan.token('id', (req) => req.id); +morgan.token('user', (req) => (req.user ? req.user.id : 'anonymous')); + +const format = + ':id :remote-addr :method :url :status :res[content-length]B - :response-time ms user=:user'; + +module.exports = morgan(format, { + stream: { write: (message) => logger.http(message.trim()) }, +}); +``` + +> Winston's default `npm` log levels (`error, warn, info, http, verbose, debug, silly`) already include `http`, so `logger.http(...)` works out of the box. + +### 4.4 `src/config/prisma.js` — Prisma client singleton with query/error logging + +```js +const { PrismaClient } = require('@prisma/client'); +const logger = require('./logger'); +const env = require('./env'); + +const prisma = new PrismaClient({ + log: [ + { emit: 'event', level: 'error' }, + { emit: 'event', level: 'warn' }, + ...(env.NODE_ENV === 'development' ? [{ emit: 'event', level: 'query' }] : []), + ], +}); + +prisma.$on('error', (e) => logger.error('Prisma error', { error: e.message })); +prisma.$on('warn', (e) => logger.warn('Prisma warning', { warning: e.message })); +if (env.NODE_ENV === 'development') { + prisma.$on('query', (e) => logger.debug('Prisma query', { query: e.query, duration: e.duration })); +} + +module.exports = prisma; +``` + +--- + +## 5. Security Implementation + +### 5.1 Authentication — JWT access + rotating refresh tokens + +- **Access token**: short-lived (default 15 min), sent in `Authorization: Bearer ` header, carries `{ sub: user.id, role_id }`. +- **Refresh token**: long-lived (default 7 days), random opaque string. Only its **hash** is stored in the `refresh_tokens` table (never the raw token). On refresh, the old token is revoked and a new one issued (rotation) — prevents replay if a token is leaked. +- **Password hashing**: bcrypt, configurable cost factor (`BCRYPT_SALT_ROUNDS`, default 12). +- **Account lockout**: `users.failed_login_attempts` and `users.locked_until` columns. After `MAX_LOGIN_ATTEMPTS` consecutive failures, lock for `LOCKOUT_DURATION_MINUTES`. Reset counter on successful login. +- **Password reset**: one-time token (random, hashed, short expiry) stored in `password_reset_tokens`, sent via email/SMS — never return the raw token in API responses outside the dedicated flow. + +Prisma models to add (in addition to the Phase 1 business tables): + +```prisma +model RefreshToken { + id BigInt @id @default(autoincrement()) + user_id BigInt + token_hash String @unique + expires_at DateTime + revoked_at DateTime? + created_at DateTime @default(now()) + + user User @relation(fields: [user_id], references: [id]) + + @@map("refresh_tokens") +} + +model PasswordResetToken { + id BigInt @id @default(autoincrement()) + user_id BigInt + token_hash String @unique + expires_at DateTime + used_at DateTime? + created_at DateTime @default(now()) + + @@map("password_reset_tokens") +} +``` + +### 5.2 `src/middlewares/auth.middleware.js` — verify JWT, load user + permissions + +```js +const jwt = require('jsonwebtoken'); +const ApiError = require('../utils/ApiError'); +const env = require('../config/env'); +const prisma = require('../config/prisma'); + +module.exports = async (req, res, next) => { + try { + const header = req.headers.authorization; + if (!header || !header.startsWith('Bearer ')) { + throw new ApiError(401, 'Authentication token missing'); + } + + const token = header.split(' ')[1]; + const payload = jwt.verify(token, env.JWT_ACCESS_SECRET); + + const user = await prisma.user.findFirst({ + where: { id: BigInt(payload.sub), deleted_at: null }, + include: { + role: { + include: { role_permissions: { include: { permission: { include: { module: true } } } } }, + }, + }, + }); + + if (!user || !user.is_active || user.status !== 'active') { + throw new ApiError(401, 'Invalid or inactive user'); + } + + req.user = user; + next(); + } catch (err) { + if (err.name === 'TokenExpiredError') return next(new ApiError(401, 'Access token expired')); + if (err.name === 'JsonWebTokenError') return next(new ApiError(401, 'Invalid access token')); + next(err instanceof ApiError ? err : new ApiError(401, 'Unauthorized')); + } +}; +``` + +### 5.3 `src/middlewares/rbac.middleware.js` — module/action permission check + +```js +const ApiError = require('../utils/ApiError'); + +const authorize = (moduleCode, action) => (req, res, next) => { + const permissions = req.user?.role?.role_permissions || []; + + const allowed = permissions.some( + (rp) => rp.permission.module.code === moduleCode && rp.permission.action === action + ); + + if (!allowed) { + return next(new ApiError(403, `Forbidden: requires ${moduleCode}:${action}`)); + } + + next(); +}; + +module.exports = authorize; +``` + +Usage in routes: + +```js +router.post('/', authenticate, authorize('VENDOR', 'create'), validate(createVendorSchema), controller.create); +``` + +Permission actions per module: `view`, `create`, `edit`, `delete`, `approve`, `export` — matching the `permissions` table defined in the requirements document. + +### 5.4 `src/middlewares/validate.middleware.js` — Joi request validation + +```js +const ApiError = require('../utils/ApiError'); + +const validate = (schema, source = 'body') => (req, res, next) => { + const { error, value } = schema.validate(req[source], { abortEarly: false, stripUnknown: true }); + + if (error) { + const errors = error.details.map((d) => ({ field: d.path.join('.'), message: d.message })); + return next(new ApiError(422, 'Validation failed', errors)); + } + + req[source] = value; + next(); +}; + +module.exports = validate; +``` + +Usage: `validate(createVendorSchema)` for body (default), `validate(listQuerySchema, 'query')` for query params. + +### 5.5 `src/middlewares/rateLimiter.middleware.js` + +```js +const rateLimit = require('express-rate-limit'); +const env = require('../config/env'); + +const generalLimiter = rateLimit({ + windowMs: env.RATE_LIMIT_WINDOW_MS, + max: env.RATE_LIMIT_MAX, + standardHeaders: true, + legacyHeaders: false, + message: { success: false, message: 'Too many requests, please try again later' }, +}); + +const authLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: env.AUTH_RATE_LIMIT_MAX, + standardHeaders: true, + legacyHeaders: false, + skipSuccessfulRequests: true, + message: { success: false, message: 'Too many login attempts, please try again later' }, +}); + +module.exports = { generalLimiter, authLimiter }; +``` + +`generalLimiter` is applied to all `/api` routes; `authLimiter` additionally applied to `/api/v1/auth/login` and `/api/v1/auth/forgot-password`. + +### 5.6 Security headers, CORS, HPP, compression + +Configured centrally in `app.js` (Section 8): + +- **Helmet**: sets `X-Content-Type-Options`, `X-Frame-Options`, `Strict-Transport-Security`, removes `X-Powered-By`, and applies a Content-Security-Policy. CSP must be relaxed (or scoped) for the `/api-docs` route since Swagger UI loads inline scripts/styles. +- **CORS**: origins restricted to `CORS_ORIGINS` (comma-separated env list) — never `*` in production. `credentials: true` only if refresh tokens are ever issued via httpOnly cookies (web admin). +- **HPP**: strips duplicate query parameters (`hpp` package) to prevent HTTP parameter pollution. +- **Compression**: gzip responses via `compression` middleware. + +### 5.7 Field-level encryption — `src/utils/encryption.js` + +For sensitive PII (vendor bank account numbers, user mobile numbers, etc.): encrypt with AES-256-GCM at rest, and maintain an HMAC-SHA256 **blind index** column (e.g. `mobile_index`) for equality search, exactly as done in the CI4 project. + +```js +const crypto = require('crypto'); +const env = require('../config/env'); + +const ALGORITHM = 'aes-256-gcm'; +const KEY = Buffer.from(env.ENCRYPTION_KEY, 'hex'); // 32 bytes + +const encrypt = (plainText) => { + if (plainText === null || plainText === undefined || plainText === '') return null; + const iv = crypto.randomBytes(12); + const cipher = crypto.createCipheriv(ALGORITHM, KEY, iv); + const encrypted = Buffer.concat([cipher.update(String(plainText), 'utf8'), cipher.final()]); + const authTag = cipher.getAuthTag(); + return Buffer.concat([iv, authTag, encrypted]).toString('base64'); +}; + +const decrypt = (payload) => { + if (!payload) return null; + const buffer = Buffer.from(payload, 'base64'); + const iv = buffer.subarray(0, 12); + const authTag = buffer.subarray(12, 28); + const encrypted = buffer.subarray(28); + const decipher = crypto.createDecipheriv(ALGORITHM, KEY, iv); + decipher.setAuthTag(authTag); + return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf8'); +}; + +const blindIndex = (value) => { + if (!value) return null; + return crypto + .createHmac('sha256', env.ENCRYPTION_HMAC_KEY) + .update(String(value).toLowerCase().trim()) + .digest('hex'); +}; + +module.exports = { encrypt, decrypt, blindIndex }; +``` + +Apply this in the service layer when writing/reading `users.mobile`, `vendor_bank_details.account_number`, etc. — store both the encrypted value and its blind-index column, and query on the blind index. + +### 5.8 File uploads — `src/middlewares/upload.middleware.js` + +```js +const multer = require('multer'); +const path = require('path'); +const crypto = require('crypto'); +const ApiError = require('../utils/ApiError'); +const env = require('../config/env'); + +const ALLOWED_MIME = ['application/pdf', 'image/jpeg', 'image/png', 'image/webp']; + +const storage = multer.diskStorage({ + destination: (req, file, cb) => cb(null, env.UPLOAD_DIR), + filename: (req, file, cb) => { + const unique = crypto.randomBytes(16).toString('hex'); + cb(null, `${unique}${path.extname(file.originalname).toLowerCase()}`); + }, +}); + +const fileFilter = (req, file, cb) => { + if (!ALLOWED_MIME.includes(file.mimetype)) { + return cb(new ApiError(400, `Unsupported file type: ${file.mimetype}`), false); + } + cb(null, true); +}; + +module.exports = multer({ + storage, + fileFilter, + limits: { fileSize: env.MAX_FILE_SIZE_MB * 1024 * 1024 }, +}); +``` + +Notes: filenames are randomized (never trust the original name), the upload directory is **not** directly web-served — files are streamed through an authenticated, RBAC-checked download endpoint, and MIME type is validated against an allow-list (not the file extension alone). + +### 5.9 Audit logging — `src/utils/auditLog.js` + +```js +const prisma = require('../config/prisma'); + +module.exports = async ({ tableName, recordId, action, oldValue, newValue, userId, requestId }) => { + await prisma.auditLog.create({ + data: { + table_name: tableName, + record_id: BigInt(recordId), + action, + old_value: oldValue ?? undefined, + new_value: newValue ?? undefined, + performed_by: userId ? BigInt(userId) : null, + request_id: requestId, + }, + }); +}; +``` + +Every create/update/status-change/approve/reject action in every service calls `auditLog(...)`. Prisma model: + +```prisma +model AuditLog { + id BigInt @id @default(autoincrement()) + table_name String + record_id BigInt + action String + old_value Json? + new_value Json? + performed_by BigInt? + request_id String? + performed_at DateTime @default(now()) + + @@map("audit_logs") + @@index([table_name, record_id]) +} +``` + +--- + +## 6. Error Handling & Response Format + +### 6.1 `src/utils/ApiError.js` + +```js +class ApiError extends Error { + constructor(statusCode, message, errors = [], isOperational = true) { + super(message); + this.statusCode = statusCode; + this.errors = errors; + this.isOperational = isOperational; + Error.captureStackTrace(this, this.constructor); + } +} + +module.exports = ApiError; +``` + +### 6.2 `src/utils/ApiResponse.js` + +```js +class ApiResponse { + constructor(statusCode, data = null, message = 'Success', meta = null) { + this.success = statusCode < 400; + this.message = message; + if (data !== null) this.data = data; + if (meta !== null) this.meta = meta; + } +} + +module.exports = ApiResponse; +``` + +Standard success envelope: + +```json +{ + "success": true, + "message": "Vendors fetched", + "data": [ ... ], + "meta": { "page": 1, "limit": 20, "total": 57 } +} +``` + +Standard error envelope: + +```json +{ + "success": false, + "message": "Validation failed", + "errors": [{ "field": "gstin", "message": "\"gstin\" length must be 15 characters long" }] +} +``` + +### 6.3 `src/utils/asyncHandler.js` + +```js +module.exports = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next); +``` + +### 6.4 `src/middlewares/error.middleware.js` — global error handler (registered last) + +```js +const ApiError = require('../utils/ApiError'); +const logger = require('../config/logger'); +const env = require('../config/env'); + +module.exports = (err, req, res, next) => { + let statusCode = err.statusCode || 500; + let message = err.message || 'Internal server error'; + let errors = err.errors || []; + + if (!(err instanceof ApiError)) { + statusCode = 500; + message = env.NODE_ENV === 'production' ? 'Internal server error' : err.message; + errors = []; + } + + logger.error(message, { + requestId: req.id, + statusCode, + path: req.originalUrl, + method: req.method, + user: req.user ? req.user.id.toString() : undefined, + stack: err.stack, + }); + + res.status(statusCode).json({ + success: false, + message, + errors, + ...(env.NODE_ENV === 'development' && { stack: err.stack }), + }); +}; +``` + +### 6.5 `src/middlewares/requestId.middleware.js` + +```js +const { v4: uuidv4 } = require('uuid'); + +module.exports = (req, res, next) => { + req.id = req.headers['x-request-id'] || uuidv4(); + res.setHeader('X-Request-Id', req.id); + next(); +}; +``` + +Every log line and audit log entry carries `request_id`, so a single request can be traced end-to-end across the access log, error log, and audit trail. + +--- + +## 7. App & Server Bootstrap + +### 7.1 `src/app.js` + +```js +const express = require('express'); +const helmet = require('helmet'); +const cors = require('cors'); +const hpp = require('hpp'); +const compression = require('compression'); +const swaggerUi = require('swagger-ui-express'); + +const env = require('./config/env'); +const swaggerSpec = require('./config/swagger'); +const morganMiddleware = require('./config/morgan'); +const requestId = require('./middlewares/requestId.middleware'); +const { generalLimiter } = require('./middlewares/rateLimiter.middleware'); +const errorMiddleware = require('./middlewares/error.middleware'); +const ApiError = require('./utils/ApiError'); +const routesV1 = require('./routes/v1'); + +const app = express(); + +app.use(helmet()); +app.use(cors({ origin: env.CORS_ORIGINS.split(',').map((o) => o.trim()), credentials: true })); +app.use(hpp()); +app.use(compression()); +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true })); +app.use(requestId); +app.use(morganMiddleware); + +app.get('/health', (req, res) => res.json({ success: true, message: 'OK', uptime: process.uptime() })); + +app.use('/api', generalLimiter); +app.use('/api/v1', routesV1); + +app.use( + '/api-docs', + helmet({ contentSecurityPolicy: false }), + swaggerUi.serve, + swaggerUi.setup(swaggerSpec) +); + +app.use((req, res, next) => next(new ApiError(404, `Route not found: ${req.originalUrl}`))); +app.use(errorMiddleware); + +module.exports = app; +``` + +### 7.2 `src/server.js` + +```js +const app = require('./app'); +const env = require('./config/env'); +const logger = require('./config/logger'); +const prisma = require('./config/prisma'); + +const server = app.listen(env.PORT, () => { + logger.info(`Server running on port ${env.PORT} [${env.NODE_ENV}]`); +}); + +const shutdown = async (signal) => { + logger.info(`${signal} received — shutting down gracefully`); + server.close(async () => { + await prisma.$disconnect(); + logger.info('HTTP server closed, Prisma disconnected'); + process.exit(0); + }); +}; + +process.on('SIGTERM', () => shutdown('SIGTERM')); +process.on('SIGINT', () => shutdown('SIGINT')); + +process.on('unhandledRejection', (reason) => { + logger.error('Unhandled promise rejection', { reason: reason?.message || reason }); +}); + +process.on('uncaughtException', (err) => { + logger.error('Uncaught exception — exiting', { error: err.message, stack: err.stack }); + process.exit(1); +}); +``` + +--- + +## 8. Prisma & Database Conventions + +- **Naming**: Prisma models in `PascalCase` (singular), mapped to `snake_case` plural tables via `@@map`. Fields stay `snake_case` to match the schema in the requirements document — Prisma's `camelCase` is not required since we're using raw field names directly. +- **Common columns**: every business table includes `is_active`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at` (nullable, soft delete). Example: + +```prisma +model Vendor { + id BigInt @id @default(autoincrement()) + vendor_code String @unique + vendor_name String + vendor_type String + gstin String? + pan String? + status String @default("active") // active | inactive | blacklisted + is_active Boolean @default(true) + created_by BigInt? + updated_by BigInt? + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + deleted_at DateTime? + + @@map("vendors") +} +``` + +- **Soft delete everywhere on transactional tables**: services filter `deleted_at: null`; a "delete" endpoint sets `deleted_at = now()`, never `DELETE FROM`. +- **Migration workflow**: + +```bash +npx prisma migrate dev --name init +npx prisma generate +node prisma/seed.js +``` + +### 8.1 `prisma/seed.js` — seed modules, permissions, roles, default admin + +The seed script must create, in order: + +1. `modules` rows for every Phase 1 module: `USERS`, `ROLES`, `MASTERS`, `VENDOR`, `PURCHASE_ORDER`, `GRN`, `ASSET`. +2. `permissions` rows: each module × `[view, create, edit, delete, approve, export]` (skip `approve`/`export` where not applicable). +3. `roles`: `Super Admin` (all permissions), `Admin`, `Purchase Manager`, `Store Manager`, `Accounts`, `Asset Manager`. +4. `role_permissions` mapping per the matrix agreed with the client. +5. One bootstrap user with role `Super Admin`, password hashed with bcrypt, `status = active`. + +Run idempotently (`upsert`, not `create`) so re-running the seed doesn't duplicate data. + +--- + +## 9. HMVC Module Pattern + +### 9.1 Layer responsibilities + +| Layer | File | Responsibility | +|---|---|---| +| Routes | `*.routes.js` | Maps HTTP verb + path → middleware chain (`authenticate`, `authorize`, `validate`) → controller method | +| Controller | `*.controller.js` | Parses request, calls service, shapes `ApiResponse`. No business logic, no Prisma calls. | +| Service | `*.service.js` | Business logic, calls Prisma (directly for simple CRUD) or the repository (for multi-table transactions), calls `auditLog`. | +| Repository | `*.repository.js` | (Complex modules only — PO, GRN, Assets) Encapsulates multi-table Prisma transactions (`prisma.$transaction`). | +| Validation | `*.validation.js` | Joi schemas for create/update/query. | + +### 9.2 Full example — Auth module + +**`src/modules/auth/auth.validation.js`** + +```js +const Joi = require('joi'); + +const loginSchema = Joi.object({ + email: Joi.string().email().required(), + password: Joi.string().min(8).required(), +}); + +const refreshSchema = Joi.object({ + refresh_token: Joi.string().required(), +}); + +module.exports = { loginSchema, refreshSchema }; +``` + +**`src/modules/auth/auth.service.js`** + +```js +const bcrypt = require('bcrypt'); +const jwt = require('jsonwebtoken'); +const crypto = require('crypto'); +const prisma = require('../../config/prisma'); +const ApiError = require('../../utils/ApiError'); +const env = require('../../config/env'); + +const hashToken = (token) => crypto.createHash('sha256').update(token).digest('hex'); + +const issueTokens = async (user) => { + const accessToken = jwt.sign({ sub: user.id.toString(), role_id: user.role_id?.toString() }, env.JWT_ACCESS_SECRET, { + expiresIn: env.JWT_ACCESS_EXPIRY, + }); + + const refreshToken = crypto.randomBytes(40).toString('hex'); + await prisma.refreshToken.create({ + data: { + user_id: user.id, + token_hash: hashToken(refreshToken), + expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), + }, + }); + + return { accessToken, refreshToken }; +}; + +const login = async (email, password) => { + const user = await prisma.user.findFirst({ where: { email, deleted_at: null } }); + if (!user) throw new ApiError(401, 'Invalid email or password'); + + if (user.locked_until && user.locked_until > new Date()) { + throw new ApiError(423, 'Account locked due to too many failed attempts. Try again later.'); + } + + const match = await bcrypt.compare(password, user.password_hash); + if (!match) { + const attempts = user.failed_login_attempts + 1; + const lockout = attempts >= env.MAX_LOGIN_ATTEMPTS; + await prisma.user.update({ + where: { id: user.id }, + data: { + failed_login_attempts: lockout ? 0 : attempts, + locked_until: lockout ? new Date(Date.now() + env.LOCKOUT_DURATION_MINUTES * 60 * 1000) : null, + }, + }); + throw new ApiError(401, 'Invalid email or password'); + } + + if (user.status !== 'active' || !user.is_active) { + throw new ApiError(403, 'Account is inactive. Contact administrator.'); + } + + await prisma.user.update({ + where: { id: user.id }, + data: { failed_login_attempts: 0, locked_until: null, last_login_at: new Date() }, + }); + + return issueTokens(user); +}; + +const refresh = async (refreshToken) => { + const tokenHash = hashToken(refreshToken); + const record = await prisma.refreshToken.findFirst({ where: { token_hash: tokenHash, revoked_at: null } }); + + if (!record || record.expires_at < new Date()) { + throw new ApiError(401, 'Invalid or expired refresh token'); + } + + await prisma.refreshToken.update({ where: { id: record.id }, data: { revoked_at: new Date() } }); + + const user = await prisma.user.findUnique({ where: { id: record.user_id } }); + if (!user || !user.is_active) throw new ApiError(401, 'User not found or inactive'); + + return issueTokens(user); +}; + +const logout = async (refreshToken) => { + const tokenHash = hashToken(refreshToken); + await prisma.refreshToken.updateMany({ + where: { token_hash: tokenHash, revoked_at: null }, + data: { revoked_at: new Date() }, + }); +}; + +module.exports = { login, refresh, logout }; +``` + +**`src/modules/auth/auth.controller.js`** + +```js +const asyncHandler = require('../../utils/asyncHandler'); +const ApiResponse = require('../../utils/ApiResponse'); +const authService = require('./auth.service'); + +const login = asyncHandler(async (req, res) => { + const tokens = await authService.login(req.body.email, req.body.password); + res.json(new ApiResponse(200, tokens, 'Login successful')); +}); + +const refresh = asyncHandler(async (req, res) => { + const tokens = await authService.refresh(req.body.refresh_token); + res.json(new ApiResponse(200, tokens, 'Token refreshed')); +}); + +const logout = asyncHandler(async (req, res) => { + await authService.logout(req.body.refresh_token); + res.json(new ApiResponse(200, null, 'Logged out')); +}); + +module.exports = { login, refresh, logout }; +``` + +**`src/modules/auth/auth.routes.js`** + +```js +const express = require('express'); +const validate = require('../../middlewares/validate.middleware'); +const { authLimiter } = require('../../middlewares/rateLimiter.middleware'); +const { loginSchema, refreshSchema } = require('./auth.validation'); +const controller = require('./auth.controller'); + +const router = express.Router(); + +router.post('/login', authLimiter, validate(loginSchema), controller.login); +router.post('/refresh', validate(refreshSchema), controller.refresh); +router.post('/logout', validate(refreshSchema), controller.logout); + +module.exports = router; +``` + +### 9.3 Full example — Vendors module (transactional CRUD with RBAC + audit) + +**`src/modules/vendors/vendors.validation.js`** + +```js +const Joi = require('joi'); + +const createVendorSchema = Joi.object({ + vendor_name: Joi.string().max(200).required(), + vendor_type: Joi.string().valid('RAW_MATERIAL', 'PACKING_MATERIAL', 'ASSET_CAPITAL', 'SERVICE', 'GENERAL').required(), + gstin: Joi.string().length(15).allow('').optional(), + pan: Joi.string().length(10).allow('').optional(), + payment_term_id: Joi.number().integer().optional(), + credit_period_days: Joi.number().integer().min(0).default(0), + remarks: Joi.string().allow('').optional(), +}); + +const updateVendorSchema = createVendorSchema.fork( + Object.keys(createVendorSchema.describe().keys), + (s) => s.optional() +); + +const statusSchema = Joi.object({ + status: Joi.string().valid('active', 'inactive', 'blacklisted').required(), +}); + +module.exports = { createVendorSchema, updateVendorSchema, statusSchema }; +``` + +**`src/modules/vendors/vendors.service.js`** + +```js +const prisma = require('../../config/prisma'); +const ApiError = require('../../utils/ApiError'); +const auditLog = require('../../utils/auditLog'); +const { nextDocumentNumber } = require('../../utils/generateCode'); + +const createVendor = async (data, userId, requestId) => { + const vendor_code = await nextDocumentNumber('VENDOR'); + + const vendor = await prisma.vendor.create({ + data: { ...data, vendor_code, created_by: userId, updated_by: userId }, + }); + + await auditLog({ tableName: 'vendors', recordId: vendor.id, action: 'CREATE', newValue: vendor, userId, requestId }); + return vendor; +}; + +const getVendors = async ({ page, limit, search, status }) => { + const where = { + deleted_at: null, + ...(status && { status }), + ...(search && { vendor_name: { contains: search, mode: 'insensitive' } }), + }; + + const [data, total] = await Promise.all([ + prisma.vendor.findMany({ where, skip: (page - 1) * limit, take: limit, orderBy: { created_at: 'desc' } }), + prisma.vendor.count({ where }), + ]); + + return { data, total }; +}; + +const getVendorById = async (id) => { + const vendor = await prisma.vendor.findFirst({ where: { id: BigInt(id), deleted_at: null } }); + if (!vendor) throw new ApiError(404, 'Vendor not found'); + return vendor; +}; + +const updateVendor = async (id, data, userId, requestId) => { + const existing = await getVendorById(id); + + const vendor = await prisma.vendor.update({ + where: { id: BigInt(id) }, + data: { ...data, updated_by: userId }, + }); + + await auditLog({ + tableName: 'vendors', + recordId: id, + action: 'UPDATE', + oldValue: existing, + newValue: vendor, + userId, + requestId, + }); + + return vendor; +}; + +const setVendorStatus = async (id, status, userId, requestId) => { + const existing = await getVendorById(id); + + if (existing.status === 'blacklisted' && status !== 'blacklisted') { + // Reactivating a blacklisted vendor — flagged for confirmation: should this + // require an additional approval permission (VENDOR:approve)? + } + + const vendor = await prisma.vendor.update({ where: { id: BigInt(id) }, data: { status, updated_by: userId } }); + + await auditLog({ + tableName: 'vendors', + recordId: id, + action: 'STATUS_CHANGE', + oldValue: { status: existing.status }, + newValue: { status }, + userId, + requestId, + }); + + return vendor; +}; + +module.exports = { createVendor, getVendors, getVendorById, updateVendor, setVendorStatus }; +``` + +**`src/modules/vendors/vendors.controller.js`** + +```js +const asyncHandler = require('../../utils/asyncHandler'); +const ApiResponse = require('../../utils/ApiResponse'); +const service = require('./vendors.service'); + +const create = asyncHandler(async (req, res) => { + const vendor = await service.createVendor(req.body, req.user.id, req.id); + res.status(201).json(new ApiResponse(201, vendor, 'Vendor created successfully')); +}); + +const list = asyncHandler(async (req, res) => { + const page = Number(req.query.page) || 1; + const limit = Math.min(Number(req.query.limit) || 20, 100); + const { data, total } = await service.getVendors({ page, limit, search: req.query.search, status: req.query.status }); + res.json(new ApiResponse(200, data, 'Vendors fetched', { page, limit, total })); +}); + +const getOne = asyncHandler(async (req, res) => { + const vendor = await service.getVendorById(req.params.id); + res.json(new ApiResponse(200, vendor, 'Vendor fetched')); +}); + +const update = asyncHandler(async (req, res) => { + const vendor = await service.updateVendor(req.params.id, req.body, req.user.id, req.id); + res.json(new ApiResponse(200, vendor, 'Vendor updated successfully')); +}); + +const changeStatus = asyncHandler(async (req, res) => { + const vendor = await service.setVendorStatus(req.params.id, req.body.status, req.user.id, req.id); + res.json(new ApiResponse(200, vendor, 'Vendor status updated')); +}); + +module.exports = { create, list, getOne, update, changeStatus }; +``` + +**`src/modules/vendors/vendors.routes.js`** + +```js +const express = require('express'); +const authenticate = require('../../middlewares/auth.middleware'); +const authorize = require('../../middlewares/rbac.middleware'); +const validate = require('../../middlewares/validate.middleware'); +const controller = require('./vendors.controller'); +const { createVendorSchema, updateVendorSchema, statusSchema } = require('./vendors.validation'); + +const router = express.Router(); + +router.use(authenticate); + +router.get('/', authorize('VENDOR', 'view'), controller.list); +router.get('/:id', authorize('VENDOR', 'view'), controller.getOne); +router.post('/', authorize('VENDOR', 'create'), validate(createVendorSchema), controller.create); +router.put('/:id', authorize('VENDOR', 'edit'), validate(updateVendorSchema), controller.update); +router.patch('/:id/status', authorize('VENDOR', 'edit'), validate(statusSchema), controller.changeStatus); + +module.exports = router; +``` + +### 9.4 Masters module pattern + +Every master (UOM, Item Categories, Brands, GST Rates, Warehouses, Payment Terms, Delivery Terms, Asset Categories, Departments, Designations, Plants, etc.) follows the **same four-file pattern as Vendors**, scaled down — simple `name`/`code` fields, no document-number generation, RBAC permission code `MASTERS`. Build one master fully (UOM is simplest) as the template, then replicate for the rest. + +`src/modules/masters/index.js` aggregates all sub-routers: + +```js +const express = require('express'); +const router = express.Router(); + +router.use('/uom', require('./uom/uom.routes')); +router.use('/item-categories', require('./item-categories/item-categories.routes')); +router.use('/item-subcategories', require('./item-subcategories/item-subcategories.routes')); +router.use('/items', require('./items/items.routes')); +router.use('/brands', require('./brands/brands.routes')); +router.use('/gst-rates', require('./gst-rates/gst-rates.routes')); +router.use('/warehouses', require('./warehouses/warehouses.routes')); +router.use('/payment-terms', require('./payment-terms/payment-terms.routes')); +router.use('/delivery-terms', require('./delivery-terms/delivery-terms.routes')); +router.use('/asset-categories', require('./asset-categories/asset-categories.routes')); +router.use('/departments', require('./departments/departments.routes')); +router.use('/designations', require('./designations/designations.routes')); +router.use('/plants', require('./plants/plants.routes')); +router.use('/document-series', require('./document-series/document-series.routes')); + +module.exports = router; +``` + +### 9.5 Document numbering — `src/utils/generateCode.js` + +Backs the `document_series` master (PO numbers, GRN numbers, vendor codes, asset codes). Use a DB transaction with `SELECT ... FOR UPDATE` (or an atomic `UPDATE ... RETURNING`) on the series row to avoid duplicate numbers under concurrent requests: + +```js +const prisma = require('../config/prisma'); + +const nextDocumentNumber = async (seriesCode) => + prisma.$transaction(async (tx) => { + const series = await tx.documentSeries.findUnique({ where: { code: seriesCode } }); + if (!series) throw new Error(`Document series not configured: ${seriesCode}`); + + const nextSeq = series.current_number + 1; + await tx.documentSeries.update({ where: { code: seriesCode }, data: { current_number: nextSeq } }); + + const padded = String(nextSeq).padStart(series.padding || 5, '0'); + return `${series.prefix}${padded}`; + }); + +module.exports = { nextDocumentNumber }; +``` + +### 9.6 Purchase Orders, GRN, Assets + +These follow the Vendors pattern but with a `*.repository.js` for multi-table writes: + +- **Purchase Orders**: `purchase-orders.repository.js` wraps creating the PO header + line items in one `prisma.$transaction`. Status-change endpoints (`submit`, `approve`, `reject`, `amend`, `cancel`) each call `auditLog` and check `authorize('PURCHASE_ORDER', 'approve')` for the approval endpoints specifically (separate from `edit`). +- **GRN**: `grn.repository.js` wraps creating the GRN header + line items + updating `purchase_order_items.received_qty` + recalculating PO status, all inside one transaction. If a received item is flagged `is_asset_item`, the same transaction creates the corresponding `assets` rows. +- **Assets**: standard CRUD plus `assets.transfer` endpoint which writes an `asset_transfers` row and updates the asset's current location/department/assignee in one transaction. + +--- + +## 10. API Conventions + +- **Versioning**: all routes under `/api/v1/...`. Breaking changes go to `/api/v2`. +- **Pagination**: `?page=1&limit=20` (limit capped at 100 server-side). Response `meta: { page, limit, total }`. +- **Filtering**: `?status=approved&search=keyword` — each module documents its filterable fields. +- **Sorting**: `?sort=-created_at` (`-` prefix = descending). +- **IDs**: all primary keys are `BigInt` — serialized as strings in JSON (configure a global JSON serializer for `BigInt`, since `JSON.stringify` cannot handle it natively): + +```js +// at the top of src/server.js or app.js +BigInt.prototype.toJSON = function () { + return this.toString(); +}; +``` + +- **Dates**: ISO 8601 (`YYYY-MM-DDTHH:mm:ss.sssZ`), UTC. Convert to IST only in the Flutter client. + +--- + +## 11. Phase 1 Route Map + +``` +/api/v1/auth/login +/api/v1/auth/refresh +/api/v1/auth/logout + +/api/v1/users (CRUD, RBAC: USERS) +/api/v1/roles (CRUD + permission assignment, RBAC: ROLES) + +/api/v1/masters/uom +/api/v1/masters/item-categories +/api/v1/masters/item-subcategories +/api/v1/masters/items +/api/v1/masters/brands +/api/v1/masters/gst-rates +/api/v1/masters/warehouses +/api/v1/masters/payment-terms +/api/v1/masters/delivery-terms +/api/v1/masters/asset-categories +/api/v1/masters/departments +/api/v1/masters/designations +/api/v1/masters/plants +/api/v1/masters/document-series + +/api/v1/vendors (+ /addresses, /contacts, /bank-details sub-routes) + +/api/v1/purchase-orders (+ /submit, /approve, /reject, /amend, /cancel, /pdf) + +/api/v1/grn (+ /cancel, /pdf) + +/api/v1/assets (+ /transfer, /transfer-history) +``` + +--- + +## 12. Testing Setup + +- **Jest** as the runner, **Supertest** for HTTP-level integration tests against `app.js` (no live server needed). +- Separate test database via `DATABASE_URL` override in `.env.test`; run `prisma migrate deploy` against it before the suite. +- `tests/modules//.test.js` mirrors `src/modules//`. +- Minimum coverage for Phase 1: auth (login/refresh/lockout), RBAC middleware (allowed vs forbidden), vendor CRUD + status transitions, PO status lifecycle, GRN partial-receipt → PO status recalculation. + +```json +// jest.config.js +module.exports = { + testEnvironment: 'node', + testMatch: ['**/tests/**/*.test.js'], + setupFiles: ['dotenv/config'], +}; +``` + +--- + +## 13. Code Quality + +`.eslintrc.json`: + +```json +{ + "env": { "node": true, "es2022": true, "jest": true }, + "extends": ["eslint:recommended", "plugin:prettier/recommended"], + "parserOptions": { "ecmaVersion": 2022, "sourceType": "script" }, + "rules": { + "no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }], + "no-console": "warn" + } +} +``` + +`.prettierrc`: + +```json +{ + "semi": true, + "singleQuote": true, + "trailingComma": "es5", + "printWidth": 100 +} +``` + +Husky + lint-staged (`package.json`): + +```json +{ + "lint-staged": { + "src/**/*.js": ["eslint --fix", "prettier --write"] + } +} +``` + +--- + +## 14. API Documentation — Swagger + +`src/config/swagger.js`: + +```js +const swaggerJsdoc = require('swagger-jsdoc'); + +module.exports = swaggerJsdoc({ + definition: { + openapi: '3.0.0', + info: { title: 'ERP API', version: '1.0.0', description: 'Phase 1 — PO, GRN, Vendor, Assets, Masters, Users & RBAC' }, + servers: [{ url: '/api/v1' }], + components: { + securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' } }, + }, + security: [{ bearerAuth: [] }], + }, + apis: ['./src/modules/**/*.routes.js'], +}); +``` + +Each route file carries `@swagger` JSDoc blocks above each route definition describing path, params, request body schema, and responses. + +--- + +## 15. Docker + +`Dockerfile`: + +```dockerfile +FROM node:20-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm ci --omit=dev +COPY . . +RUN npx prisma generate +EXPOSE 3000 +CMD ["node", "src/server.js"] +``` + +`docker-compose.yml`: + +```yaml +version: "3.8" +services: + api: + build: . + ports: + - "3000:3000" + env_file: .env + depends_on: + - postgres + volumes: + - ./uploads:/app/uploads + - ./logs:/app/logs + + postgres: + image: postgres:15-alpine + environment: + POSTGRES_USER: erp_user + POSTGRES_PASSWORD: erp_password + POSTGRES_DB: erp_db + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + +volumes: + pgdata: +``` + +--- + +## 16. `package.json` — dependencies & scripts + +```json +{ + "name": "erp-backend", + "version": "1.0.0", + "main": "src/server.js", + "scripts": { + "start": "node src/server.js", + "dev": "nodemon src/server.js", + "lint": "eslint src --ext .js", + "lint:fix": "eslint src --ext .js --fix", + "format": "prettier --write \"src/**/*.js\"", + "test": "jest --runInBand", + "prisma:generate": "prisma generate", + "prisma:migrate": "prisma migrate dev", + "prisma:deploy": "prisma migrate deploy", + "prisma:seed": "node prisma/seed.js", + "prepare": "husky install" + }, + "dependencies": { + "@prisma/client": "^5.x", + "express": "^4.x", + "joi": "^17.x", + "jsonwebtoken": "^9.x", + "bcrypt": "^5.x", + "helmet": "^7.x", + "cors": "^2.x", + "hpp": "^0.2.x", + "compression": "^1.x", + "express-rate-limit": "^7.x", + "winston": "^3.x", + "winston-daily-rotate-file": "^5.x", + "morgan": "^1.x", + "multer": "^1.x", + "uuid": "^9.x", + "dotenv": "^16.x", + "swagger-jsdoc": "^6.x", + "swagger-ui-express": "^5.x" + }, + "devDependencies": { + "prisma": "^5.x", + "nodemon": "^3.x", + "eslint": "^8.x", + "eslint-config-prettier": "^9.x", + "eslint-plugin-prettier": "^5.x", + "prettier": "^3.x", + "jest": "^29.x", + "supertest": "^6.x", + "husky": "^9.x", + "lint-staged": "^15.x" + } +} +``` + +--- + +## 17. Security Checklist (OWASP-mapped) + +| Concern | Mitigation in this setup | +|---|---| +| SQL injection | Prisma parameterized queries everywhere; never raw string-concatenated SQL | +| Broken authentication | bcrypt password hashing, short-lived JWT access tokens, rotating hashed refresh tokens, account lockout after repeated failures | +| Broken access control | `authenticate` + `authorize(module, action)` on every protected route; soft-deleted/inactive users rejected at auth | +| Sensitive data exposure | AES-256-GCM field encryption + HMAC blind index for PII; secrets only in `.env` (never committed); Winston redacts password/token fields | +| Security misconfiguration | Helmet security headers, env validated on boot (fails fast if misconfigured), no default credentials in seed beyond a documented bootstrap admin | +| Brute force / DoS | `express-rate-limit` global + stricter limiter on `/auth/login` | +| Cross-site scripting (XSS) | Joi input validation/sanitization on every endpoint; JSON-only API (no server-rendered HTML); Flutter client responsible for output encoding | +| CSRF | Not applicable for header-based Bearer JWT (no cookies); if a web admin later uses httpOnly cookies for refresh tokens, add `csurf` / double-submit cookie pattern | +| Insecure file upload | Multer MIME allow-list, random filenames, size limit, files served only via authenticated/RBAC-checked endpoints | +| Insufficient logging & monitoring | Winston (console + daily-rotated error/combined logs), Morgan HTTP access logs, `audit_logs` table for all data-changing actions, `X-Request-Id` correlation across logs | +| Dependency vulnerabilities | Run `npm audit` in CI; keep `package-lock.json` committed | + +--- + +## 18. Build Order (for Cursor) + +1. `package.json`, install dependencies, `.env.example`, `.eslintrc.json`, `.prettierrc`. +2. `prisma/schema.prisma` — start with `User`, `Role`, `Module`, `Permission`, `RolePermission`, `RefreshToken`, `PasswordResetToken`, `AuditLog`, `DocumentSeries`, then add the Phase 1 business tables (Vendor + sub-tables, PurchaseOrder + items, Grn + items, Asset + transfers, all masters) from the requirements document. +3. `src/config/*` (env, logger, morgan, prisma, swagger). +4. `src/utils/*` (ApiError, ApiResponse, asyncHandler, encryption, auditLog, generateCode, pagination). +5. `src/middlewares/*` (requestId, error, validate, rateLimiter, auth, rbac, upload). +6. `src/app.js`, `src/server.js`, confirm `/health` responds. +7. `prisma/seed.js` — modules, permissions, roles, bootstrap admin. +8. `auth` module end-to-end (login/refresh/logout) — confirm JWT + RBAC works against a protected test route. +9. `masters` modules (UOM first as the template, then replicate for the rest). +10. `vendors` module (full pattern with addresses/contacts/bank-details sub-resources). +11. `purchase-orders` module (header + items + approval workflow + repository transaction). +12. `grn` module (receipt + PO status recalculation + asset auto-creation transaction). +13. `assets` module (CRUD + transfer history). +14. Swagger annotations across all route files. +15. Jest test suites per module. +16. Dockerfile + docker-compose, verify `docker-compose up` boots API + Postgres. diff --git a/BACKEND_TASKS.md b/BACKEND_TASKS.md new file mode 100644 index 0000000..67a103c --- /dev/null +++ b/BACKEND_TASKS.md @@ -0,0 +1,186 @@ +# ERP Backend — Development Tasks (Phase 1) + +Reference: [BACKEND_SETUP.md](./BACKEND_SETUP.md) + +Use this checklist when building or extending the backend. Follow the build order in Section 1. + +--- + +## 1. Project Scaffold + +- [x] Create `package.json` with dependencies and scripts from the spec +- [x] Install dependencies (`npm install`) +- [x] Add `.env.example` (never commit `.env`) +- [x] Configure ESLint, Prettier, Husky, lint-staged +- [x] Add `jest.config.js` with `dotenv/config` setup file + +--- + +## 2. Database & Prisma + +- [x] Define `prisma/schema.prisma`: + - Auth: `User`, `Role`, `Module`, `Permission`, `RolePermission`, `RefreshToken`, `PasswordResetToken` + - System: `AuditLog`, `DocumentSeries` + - Business: Vendor (+ sub-tables), PurchaseOrder (+ items), GRN (+ items), Asset (+ transfers), all masters +- [x] Use `PascalCase` models, `snake_case` fields, `@@map` to plural table names +- [ ] Add common columns on every business table: `is_active`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at` +- [ ] Run `npx prisma migrate dev --name init` +- [x] Run `npx prisma generate` +- [ ] Implement idempotent `prisma/seed.js` (modules → permissions → roles → role_permissions → Super Admin user) + +### Seed requirements + +| Step | Content | +|------|---------| +| Modules | `USERS`, `ROLES`, `MASTERS`, `VENDOR`, `PURCHASE_ORDER`, `GRN`, `ASSET` | +| Permissions | Each module × `view`, `create`, `edit`, `delete`, `approve`, `export` (skip where N/A) | +| Roles | Super Admin, Admin, Purchase Manager, Store Manager, Accounts, Asset Manager | +| Bootstrap user | Super Admin, bcrypt-hashed password, `status = active` | + +--- + +## 3. Core Infrastructure + +- [x] `src/config/env.js` — Joi validation; app must refuse to boot on invalid/missing env +- [x] `src/config/logger.js` — Winston + daily rotate; redact sensitive keys +- [x] `src/config/morgan.js` — HTTP logs into Winston with request ID + user +- [x] `src/config/prisma.js` — singleton client with query/error logging +- [x] `src/config/swagger.js` — OpenAPI 3.0 from route JSDoc +- [x] `src/utils/` — ApiError, ApiResponse, asyncHandler, encryption, auditLog, pagination, generateCode +- [x] `src/middlewares/` — requestId, error, validate, rateLimiter, auth, rbac, upload +- [x] `src/app.js` — Helmet, CORS, HPP, compression, rate limit, routes, Swagger, 404, error handler +- [x] `src/server.js` — graceful shutdown, BigInt JSON serializer, unhandled rejection/exception handlers +- [ ] Verify `GET /health` responds + +--- + +## 4. Modules (HMVC Pattern) + +Build in this order. Each module: `*.routes.js` → `*.controller.js` → `*.service.js` → `*.validation.js` (+ `*.repository.js` for complex modules). + +| # | Module | Notes | +|---|--------|-------| +| 8 | **auth** | login, refresh, logout; rotating hashed refresh tokens; account lockout | +| 9 | **masters** | UOM first as template, then replicate 13 sub-masters under `masters/index.js` | +| 10 | **vendors** | CRUD + addresses/contacts/bank-details; encrypt bank account numbers | +| 11 | **purchase-orders** | Header + line items; repository for transactions; submit/approve/reject/amend/cancel | +| 12 | **grn** | Receipt + PO status recalc + asset auto-creation in one transaction | +| 13 | **assets** | CRUD + transfer + transfer history | +| — | **users** | CRUD, RBAC: `USERS` | +| — | **roles** | CRUD + permission assignment, RBAC: `ROLES` | + +Current progress: + +- [x] `auth` module scaffold (`login`, `refresh`, `logout`) implemented +- [x] `masters/uom` module implemented as template (CRUD + RBAC + validation + audit log) +- [x] Replicated masters CRUD modules: `item-categories`, `brands`, `gst-rates`, `payment-terms`, `delivery-terms`, `asset-categories`, `departments`, `designations`, `document-series` +- [x] Pending masters now completed: `item-subcategories`, `items`, `warehouses`, `plants` + +### Layer rules + +| Layer | Responsibility | +|-------|----------------| +| Routes | HTTP mapping + middleware chain (`authenticate` → `authorize` → `validate` → controller) | +| Controller | Parse request, call service, return `ApiResponse` — no business logic, no Prisma | +| Service | Business logic, Prisma (or repository), `auditLog` on every mutation | +| Repository | Multi-table `prisma.$transaction` only (PO, GRN, Assets) | +| Validation | Joi schemas for body/query | + +--- + +## 5. API Routes (Phase 1) + +``` +/api/v1/auth/{login,refresh,logout} +/api/v1/users +/api/v1/roles +/api/v1/masters/{uom,item-categories,item-subcategories,items,brands,gst-rates,warehouses,payment-terms,delivery-terms,asset-categories,departments,designations,plants,document-series} +/api/v1/vendors (+ /addresses, /contacts, /bank-details) +/api/v1/purchase-orders (+ /submit, /approve, /reject, /amend, /cancel, /pdf) +/api/v1/grn (+ /cancel, /pdf) +/api/v1/assets (+ /transfer, /transfer-history) +``` + +--- + +## 6. Security Tasks + +- [x] JWT access tokens (15m) in `Authorization: Bearer` header +- [x] Refresh tokens: opaque, SHA-256 hashed in DB, rotated on each refresh +- [x] bcrypt password hashing (`BCRYPT_SALT_ROUNDS=12`) +- [x] Account lockout after `MAX_LOGIN_ATTEMPTS` failures +- [x] `authenticate` + `authorize(module, action)` on every protected route +- [x] AES-256-GCM field encryption + HMAC blind index for searchable PII +- [x] Rate limiting: global on `/api`, stricter on `/auth/login` and `/auth/forgot-password` +- [x] Helmet, CORS (no `*` in production), HPP, compression +- [x] File uploads: MIME allow-list, random filenames, RBAC-protected download (not direct web serve) +- [ ] `auditLog()` on every create/update/status-change/approve/reject +- [x] `X-Request-Id` on every request for log correlation + +### RBAC permission actions + +`view`, `create`, `edit`, `delete`, `approve`, `export` + +--- + +## 7. API Conventions + +- Versioning: `/api/v1/...` +- Pagination: `?page=1&limit=20` (cap limit at 100); response `meta: { page, limit, total }` +- Filtering: `?status=...&search=...` per module +- Sorting: `?sort=-created_at` (`-` = descending) +- IDs: `BigInt` serialized as strings in JSON +- Dates: ISO 8601 UTC +- Soft delete: set `deleted_at`, never hard-delete transactional records +- Response envelope: `{ success, message, data?, meta?, errors? }` + +--- + +## 8. Testing + +- [ ] Jest + Supertest against `app.js` (no live server) +- [ ] Separate test DB via `.env.test` + `prisma migrate deploy` +- [ ] Mirror structure: `tests/modules//.test.js` + +### Minimum Phase 1 coverage + +- [ ] Auth: login, refresh, lockout +- [ ] RBAC: allowed vs forbidden +- [ ] Vendor: CRUD + status transitions +- [ ] PO: status lifecycle +- [ ] GRN: partial receipt → PO status recalculation + +--- + +## 9. Documentation & DevOps + +- [ ] Add `@swagger` JSDoc blocks to every route file +- [ ] `Dockerfile` (Node 20 Alpine) +- [ ] `docker-compose.yml` (API + Postgres 15) +- [ ] Verify `docker-compose up` boots API + Postgres +- [ ] Run `npm audit` in CI + +--- + +## 10. Future Development Guidelines + +When adding a new module or endpoint: + +1. Add Prisma model with common columns + soft delete +2. Add module/permissions to seed (if new domain) +3. Create the four-file HMVC module (five if multi-table transactions) +4. Register routes in `src/routes/v1/index.js` +5. Apply middleware chain: `authenticate` → `authorize(MODULE, action)` → `validate(schema)` → controller +6. Call `auditLog()` in service for all mutations +7. Use `nextDocumentNumber()` for auto-generated codes +8. Encrypt PII at service layer with blind index for search +9. Add Swagger JSDoc + Jest tests +10. Breaking API changes → new version (`/api/v2`) + +### Complex transaction modules + +Use `*.repository.js` with `prisma.$transaction` when a single operation touches multiple tables (e.g. PO header + items, GRN + PO qty update + asset creation). + +### Masters replication + +Copy the UOM module pattern for new masters: simple name/code fields, RBAC code `MASTERS`, no document numbering. diff --git a/SETUP_STATUS.md b/SETUP_STATUS.md new file mode 100644 index 0000000..64988fb --- /dev/null +++ b/SETUP_STATUS.md @@ -0,0 +1,32 @@ +# Setup Status + +Initial setup has been started with your DB config: + +- Host: `demo.venbait.in` +- Port: `5432` +- Database: `bharaterp` +- User: `bharaterpdevdbuser` + +## What is ready + +- Base Node/Express scaffold in `src/` +- Environment files (`.env`, `.env.example`) +- Prisma datasource in `prisma/schema.prisma` +- Database bootstrap script: `scripts/setup-db.sh` + +## Run DB setup + +```bash +cd /home/smart/Documents/ERP_BE +./scripts/setup-db.sh /home/smart/Downloads/erp_phase1_ddl.sql +``` + +The script: +1. Creates `bharaterp` if it does not exist +2. Executes the provided Phase 1 DDL on that database + +## Next steps + +1. Install npm dependencies (if network allows) +2. Generate Prisma client: `npm run prisma:generate` +3. Start API: `npm run dev` diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..6670aed --- /dev/null +++ b/jest.config.js @@ -0,0 +1,5 @@ +module.exports = { + testEnvironment: 'node', + testMatch: ['**/tests/**/*.test.js'], + setupFiles: ['dotenv/config'], +}; diff --git a/nodemon.json b/nodemon.json new file mode 100644 index 0000000..8faf895 --- /dev/null +++ b/nodemon.json @@ -0,0 +1,4 @@ +{ + "watch": ["src"], + "ext": "js,json,yaml,yml" +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..0e07a0a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,7737 @@ +{ + "name": "erp-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "erp-backend", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@prisma/client": "^5.22.0", + "bcrypt": "^5.1.1", + "compression": "^1.8.1", + "cors": "^2.8.5", + "dotenv": "^16.6.1", + "express": "^4.21.2", + "express-rate-limit": "^7.5.1", + "helmet": "^7.2.0", + "hpp": "^0.2.3", + "joi": "^17.13.3", + "jsonwebtoken": "^9.0.2", + "morgan": "^1.10.1", + "multer": "^1.4.5-lts.2", + "swagger-jsdoc": "6.2.8", + "swagger-ui-express": "^5.0.1", + "uuid": "^9.0.1", + "winston": "^3.18.3", + "winston-daily-rotate-file": "^5.0.0" + }, + "devDependencies": { + "eslint": "^8.57.1", + "eslint-config-prettier": "^9.1.2", + "eslint-plugin-prettier": "^5.5.4", + "husky": "^9.1.7", + "jest": "^29.7.0", + "lint-staged": "^15.5.2", + "nodemon": "^3.1.10", + "prettier": "^3.6.2", + "prisma": "^5.22.0", + "supertest": "^6.3.4" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", + "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.6", + "@apidevtools/openapi-schemas": "^2.0.4", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "z-schema": "^5.0.1" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/eslintrc/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "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.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "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 + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "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 + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==" + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@pkgr/core": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", + "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==", + "dev": true, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@prisma/client": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "hasInstallScript": true, + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "dev": true + }, + "node_modules/@prisma/engines": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "dev": true + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "dev": true, + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "dev": true, + "dependencies": { + "@prisma/debug": "5.22.0" + } + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, + "node_modules/@types/node": { + "version": "25.9.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz", + "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==", + "dev": true, + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "dev": true + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.37", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz", + "integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true + }, + "node_modules/color": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", + "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", + "dependencies": { + "color-convert": "^3.1.3", + "color-string": "^2.1.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/color-string": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-string/node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.373", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.373.tgz", + "integrity": "sha512-G2Hym8JIf/QreuseqkDibgH8Ci8KfJzqGDKdakbhSx9UltwRBH2cBLAWU/lBX0sCdv0TlhyxQyDCnSfxgMWsjA==", + "dev": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.6.tgz", + "integrity": "sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.13" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "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" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-stream-rotator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz", + "integrity": "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==", + "dependencies": { + "moment": "^2.29.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.5.tgz", + "integrity": "sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q==", + "dev": true, + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0", + "qs": "^6.11.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gauge/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/gauge/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz", + "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/hpp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/hpp/-/hpp-0.2.3.tgz", + "integrity": "sha512-4zDZypjQcxK/8pfFNR7jaON7zEUpXZxz4viyFmqjb3kWNWAHsLEUmWXcdn25c5l76ISvnD6hbOGO97cXUI3Ryw==", + "dependencies": { + "lodash": "^4.17.12", + "type-is": "^1.6.12" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "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 + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "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 + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.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.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "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 + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "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": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "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" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "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": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.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" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "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.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/joi": { + "version": "17.13.4", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.4.tgz", + "integrity": "sha512-1RuuER6kmt8K8I3nIWvPZKi5RQCb568ZPyY4Pwjlua+yo+63ZTmIwxLZH0heBmiKN4uxjvCiarDrjaeH84xicQ==", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/lint-staged": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.5.2.tgz", + "integrity": "sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w==", + "dev": true, + "dependencies": { + "chalk": "^5.4.1", + "commander": "^13.1.0", + "debug": "^4.4.0", + "execa": "^8.0.1", + "lilconfig": "^3.1.3", + "listr2": "^8.2.5", + "micromatch": "^4.0.8", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.7.0" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/lint-staged/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/lint-staged/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/lint-staged/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/lint-staged/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.3.tgz", + "integrity": "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==", + "dev": true, + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==" + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead." + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead." + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/logform/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, + "node_modules/morgan": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.11.0.tgz", + "integrity": "sha512-zSkVu3t18r39pw4ixfBKvfZi3y2UOqr7d4WYwcj3m8nXpEQK4rPO6GLzs/CExoRgmX3y9EjmmcXqv6jq0SK46g==", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.4.1", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/multer": { + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/nodemon": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/nodemon/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.1.tgz", + "integrity": "sha512-e0F9AOF1JMrCfBsyJOwU9lNvQ0WtXTq0j/4jk0BQ5JSI9VAybPXmDpPRw/2FQ3e5d3ZFN1mLh7jW99m/jjaptw==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz", + "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "engines": { + "node": "*" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/superagent": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz", + "integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==", + "deprecated": "Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net", + "dev": true, + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.1.2", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=6.4.0 <13 || >=14" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/superagent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/supertest": { + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.4.tgz", + "integrity": "sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==", + "deprecated": "Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net", + "dev": true, + "dependencies": { + "methods": "^1.1.2", + "superagent": "^8.1.2" + }, + "engines": { + "node": ">=6.4.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swagger-jsdoc": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", + "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", + "dependencies": { + "commander": "6.2.0", + "doctrine": "3.0.0", + "glob": "7.1.6", + "lodash.mergewith": "^4.6.2", + "swagger-parser": "^10.0.3", + "yaml": "2.0.0-1" + }, + "bin": { + "swagger-jsdoc": "bin/swagger-jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/swagger-jsdoc/node_modules/commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/swagger-jsdoc/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/swagger-jsdoc/node_modules/yaml": { + "version": "2.0.0-1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", + "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", + "dependencies": { + "@apidevtools/swagger-parser": "10.0.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.32.6", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.6.tgz", + "integrity": "sha512-75ttZNaYCLoFPnozPZcTUU6mS3wKT8l7WLjU5zJSHFeJa23i5vtnze6IiCl4jDMPeQTXVXIgovq4M11NNfQvSA==", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, + "node_modules/synckit": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", + "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.3.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wide-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/wide-align/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/wide-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/winston": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", + "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.8", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-daily-rotate-file": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-5.0.0.tgz", + "integrity": "sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==", + "dependencies": { + "file-stream-rotator": "^0.6.1", + "object-hash": "^3.0.0", + "triple-beam": "^1.4.1", + "winston-transport": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "winston": "^3" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/winston/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + }, + "node_modules/z-schema/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..231a694 --- /dev/null +++ b/package.json @@ -0,0 +1,60 @@ +{ + "name": "erp-backend", + "version": "1.0.0", + "description": "ERP backend (Phase 1)", + "main": "src/server.js", + "scripts": { + "start": "node src/server.js", + "dev": "nodemon src/server.js", + "lint": "eslint src --ext .js", + "lint:fix": "eslint src --ext .js --fix", + "format": "prettier --write \"src/**/*.js\"", + "test": "jest --runInBand", + "prisma:generate": "prisma generate", + "prisma:migrate": "prisma migrate dev", + "prisma:deploy": "prisma migrate deploy", + "prisma:seed": "node prisma/seed.js", + "prepare": "husky install" + }, + "lint-staged": { + "src/**/*.js": [ + "eslint --fix", + "prettier --write" + ] + }, + "dependencies": { + "@prisma/client": "^5.22.0", + "bcrypt": "^5.1.1", + "compression": "^1.8.1", + "cors": "^2.8.5", + "dotenv": "^16.6.1", + "express": "^4.21.2", + "express-rate-limit": "^7.5.1", + "helmet": "^7.2.0", + "hpp": "^0.2.3", + "joi": "^17.13.3", + "jsonwebtoken": "^9.0.2", + "morgan": "^1.10.1", + "multer": "^1.4.5-lts.2", + "swagger-jsdoc": "6.2.8", + "swagger-ui-express": "^5.0.1", + "uuid": "^9.0.1", + "winston": "^3.18.3", + "winston-daily-rotate-file": "^5.0.0" + }, + "devDependencies": { + "eslint": "^8.57.1", + "eslint-config-prettier": "^9.1.2", + "eslint-plugin-prettier": "^5.5.4", + "husky": "^9.1.7", + "jest": "^29.7.0", + "lint-staged": "^15.5.2", + "nodemon": "^3.1.10", + "prettier": "^3.6.2", + "prisma": "^5.22.0", + "supertest": "^6.3.4" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..dea8f48 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,891 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info. +model asset_attachments { + id BigInt @id @default(autoincrement()) + asset_id BigInt + attachment_type String @default("DOCUMENT") @db.VarChar(50) + file_name String @db.VarChar(255) + file_path String @db.VarChar(500) + file_type String? @db.VarChar(100) + file_size Int? + uploaded_by BigInt? + created_at DateTime @default(now()) @db.Timestamptz(6) + assets assets @relation(fields: [asset_id], references: [id], onDelete: Cascade, onUpdate: NoAction) + users users? @relation(fields: [uploaded_by], references: [id], onUpdate: NoAction) +} + +/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info. +model asset_categories { + id BigInt @id @default(autoincrement()) + code String @unique @db.VarChar(20) + name String @db.VarChar(150) + code_prefix String @unique @db.VarChar(10) + default_useful_life_years Int? + default_depreciation_method String? @db.VarChar(20) + is_active Boolean @default(true) + created_by BigInt? + updated_by BigInt? + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) + deleted_at DateTime? @db.Timestamptz(6) + users_asset_categories_created_byTousers users? @relation("asset_categories_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction) + users_asset_categories_updated_byTousers users? @relation("asset_categories_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction) + assets assets[] +} + +model asset_transfers { + id BigInt @id @default(autoincrement()) + asset_id BigInt + transfer_date DateTime @db.Date + from_plant_id BigInt? + to_plant_id BigInt? + from_department_id BigInt? + to_department_id BigInt? + from_user_id BigInt? + to_user_id BigInt? + from_warehouse_id BigInt? + to_warehouse_id BigInt? + reason String? + transferred_by BigInt? + created_at DateTime @default(now()) @db.Timestamptz(6) + assets assets @relation(fields: [asset_id], references: [id], onDelete: Cascade, onUpdate: NoAction) + departments_asset_transfers_from_department_idTodepartments departments? @relation("asset_transfers_from_department_idTodepartments", fields: [from_department_id], references: [id], onUpdate: NoAction) + plants_asset_transfers_from_plant_idToplants plants? @relation("asset_transfers_from_plant_idToplants", fields: [from_plant_id], references: [id], onUpdate: NoAction) + users_asset_transfers_from_user_idTousers users? @relation("asset_transfers_from_user_idTousers", fields: [from_user_id], references: [id], onUpdate: NoAction) + warehouses_asset_transfers_from_warehouse_idTowarehouses warehouses? @relation("asset_transfers_from_warehouse_idTowarehouses", fields: [from_warehouse_id], references: [id], onUpdate: NoAction) + departments_asset_transfers_to_department_idTodepartments departments? @relation("asset_transfers_to_department_idTodepartments", fields: [to_department_id], references: [id], onUpdate: NoAction) + plants_asset_transfers_to_plant_idToplants plants? @relation("asset_transfers_to_plant_idToplants", fields: [to_plant_id], references: [id], onUpdate: NoAction) + users_asset_transfers_to_user_idTousers users? @relation("asset_transfers_to_user_idTousers", fields: [to_user_id], references: [id], onUpdate: NoAction) + warehouses_asset_transfers_to_warehouse_idTowarehouses warehouses? @relation("asset_transfers_to_warehouse_idTowarehouses", fields: [to_warehouse_id], references: [id], onUpdate: NoAction) + users_asset_transfers_transferred_byTousers users? @relation("asset_transfers_transferred_byTousers", fields: [transferred_by], references: [id], onUpdate: NoAction) + + @@index([asset_id], map: "idx_asset_transfers_asset_id") +} + +/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info. +model assets { + id BigInt @id @default(autoincrement()) + asset_code String @unique @db.VarChar(30) + asset_name String @db.VarChar(200) + asset_category_id BigInt + brand_model String? @db.VarChar(200) + manufacturer String? @db.VarChar(200) + serial_number String? @db.VarChar(100) + part_number String? @db.VarChar(100) + plant_id BigInt + department_id BigInt? + warehouse_id BigInt? + location_detail String? @db.VarChar(200) + assigned_to_user_id BigInt? + vendor_id BigInt? + po_id BigInt? + grn_id BigInt? + grn_item_id BigInt? + purchase_date DateTime? @db.Date + purchase_cost Decimal @default(0) @db.Decimal(15, 4) + useful_life_years Int? + depreciation_method String? @db.VarChar(10) + salvage_value Decimal? @default(0) @db.Decimal(15, 4) + warranty_expiry_date DateTime? @db.Date + amc_start_date DateTime? @db.Date + amc_end_date DateTime? @db.Date + amc_vendor_id BigInt? + insurance_policy_no String? @db.VarChar(100) + insurance_expiry_date DateTime? @db.Date + condition String @default("NEW") @db.VarChar(20) + status String @default("IN_USE") @db.VarChar(30) + qr_code_value String? @db.VarChar(100) + disposal_date DateTime? @db.Date + disposal_reason String? + disposal_value Decimal? @db.Decimal(15, 4) + remarks String? + is_active Boolean @default(true) + created_by BigInt? + updated_by BigInt? + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) + deleted_at DateTime? @db.Timestamptz(6) + asset_attachments asset_attachments[] + asset_transfers asset_transfers[] + vendors_assets_amc_vendor_idTovendors vendors? @relation("assets_amc_vendor_idTovendors", fields: [amc_vendor_id], references: [id], onUpdate: NoAction) + asset_categories asset_categories @relation(fields: [asset_category_id], references: [id], onUpdate: NoAction) + users_assets_assigned_to_user_idTousers users? @relation("assets_assigned_to_user_idTousers", fields: [assigned_to_user_id], references: [id], onUpdate: NoAction) + users_assets_created_byTousers users? @relation("assets_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction) + departments departments? @relation(fields: [department_id], references: [id], onUpdate: NoAction) + grn grn? @relation(fields: [grn_id], references: [id], onUpdate: NoAction) + grn_items grn_items? @relation(fields: [grn_item_id], references: [id], onUpdate: NoAction) + plants plants @relation(fields: [plant_id], references: [id], onUpdate: NoAction) + purchase_orders purchase_orders? @relation(fields: [po_id], references: [id], onUpdate: NoAction) + users_assets_updated_byTousers users? @relation("assets_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction) + vendors_assets_vendor_idTovendors vendors? @relation("assets_vendor_idTovendors", fields: [vendor_id], references: [id], onUpdate: NoAction) + warehouses warehouses? @relation(fields: [warehouse_id], references: [id], onUpdate: NoAction) + + @@index([asset_category_id], map: "idx_assets_category_id") + @@index([department_id], map: "idx_assets_dept_id") + @@index([plant_id], map: "idx_assets_plant_id") +} + +model audit_logs { + id BigInt @id @default(autoincrement()) + table_name String @db.VarChar(100) + record_id BigInt + action String @db.VarChar(50) + old_value Json? + new_value Json? + performed_by BigInt? + request_id String? @db.VarChar(50) + performed_at DateTime @default(now()) @db.Timestamptz(6) + users users? @relation(fields: [performed_by], references: [id], onUpdate: NoAction) + + @@index([performed_at], map: "idx_audit_logs_performed_at") + @@index([performed_by], map: "idx_audit_logs_performed_by") + @@index([table_name, record_id], map: "idx_audit_logs_table_record") +} + +/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info. +model brands { + id BigInt @id @default(autoincrement()) + code String @unique @db.VarChar(30) + name String @db.VarChar(150) + brand_type String @default("OWN") @db.VarChar(20) + contact_person String? @db.VarChar(150) + phone String? @db.VarChar(15) + email String? @db.VarChar(150) + is_active Boolean @default(true) + created_by BigInt? + updated_by BigInt? + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) + deleted_at DateTime? @db.Timestamptz(6) + users_brands_created_byTousers users? @relation("brands_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction) + users_brands_updated_byTousers users? @relation("brands_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction) + items items[] + purchase_orders purchase_orders[] +} + +model delivery_terms { + id BigInt @id @default(autoincrement()) + code String @unique @db.VarChar(30) + name String @db.VarChar(100) + description String? + is_active Boolean @default(true) + created_by BigInt? + updated_by BigInt? + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) + deleted_at DateTime? @db.Timestamptz(6) + users_delivery_terms_created_byTousers users? @relation("delivery_terms_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction) + users_delivery_terms_updated_byTousers users? @relation("delivery_terms_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction) + purchase_orders purchase_orders[] +} + +model departments { + id BigInt @id @default(autoincrement()) + name String @unique @db.VarChar(100) + is_active Boolean @default(true) + created_by BigInt? + updated_by BigInt? + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) + deleted_at DateTime? @db.Timestamptz(6) + asset_transfers_asset_transfers_from_department_idTodepartments asset_transfers[] @relation("asset_transfers_from_department_idTodepartments") + asset_transfers_asset_transfers_to_department_idTodepartments asset_transfers[] @relation("asset_transfers_to_department_idTodepartments") + assets assets[] + users users[] +} + +model designations { + id BigInt @id @default(autoincrement()) + name String @unique @db.VarChar(100) + is_active Boolean @default(true) + created_by BigInt? + updated_by BigInt? + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) + deleted_at DateTime? @db.Timestamptz(6) + users users[] +} + +model document_series { + id BigInt @id @default(autoincrement()) + code String @unique @db.VarChar(50) + prefix String @db.VarChar(30) + current_number Int @default(0) + padding Int @default(5) + description String? @db.VarChar(200) + is_active Boolean @default(true) + created_by BigInt? + updated_by BigInt? + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) + users_document_series_created_byTousers users? @relation("document_series_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction) + users_document_series_updated_byTousers users? @relation("document_series_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction) +} + +/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info. +model grn { + id BigInt @id @default(autoincrement()) + grn_number String @unique @db.VarChar(30) + grn_date DateTime @db.Date + po_id BigInt + vendor_id BigInt + warehouse_id BigInt + vendor_invoice_no String? @db.VarChar(100) + vendor_invoice_date DateTime? @db.Date + vendor_invoice_amount Decimal? @db.Decimal(15, 4) + vehicle_no String? @db.VarChar(30) + lr_no String? @db.VarChar(50) + lr_date DateTime? @db.Date + status String @default("POSTED") @db.VarChar(20) + cancellation_reason String? + cancelled_by BigInt? + cancelled_at DateTime? @db.Timestamptz(6) + received_by BigInt? + quality_checked_by BigInt? + remarks String? + is_active Boolean @default(true) + created_by BigInt? + updated_by BigInt? + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) + deleted_at DateTime? @db.Timestamptz(6) + assets assets[] + users_grn_cancelled_byTousers users? @relation("grn_cancelled_byTousers", fields: [cancelled_by], references: [id], onUpdate: NoAction) + users_grn_created_byTousers users? @relation("grn_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction) + purchase_orders purchase_orders @relation(fields: [po_id], references: [id], onUpdate: NoAction) + users_grn_quality_checked_byTousers users? @relation("grn_quality_checked_byTousers", fields: [quality_checked_by], references: [id], onUpdate: NoAction) + users_grn_received_byTousers users? @relation("grn_received_byTousers", fields: [received_by], references: [id], onUpdate: NoAction) + users_grn_updated_byTousers users? @relation("grn_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction) + vendors vendors @relation(fields: [vendor_id], references: [id], onUpdate: NoAction) + warehouses warehouses @relation(fields: [warehouse_id], references: [id], onUpdate: NoAction) + grn_attachments grn_attachments[] + grn_items grn_items[] + + @@index([grn_date], map: "idx_grn_grn_date") + @@index([po_id], map: "idx_grn_po_id") + @@index([vendor_id], map: "idx_grn_vendor_id") +} + +model grn_attachments { + id BigInt @id @default(autoincrement()) + grn_id BigInt + file_name String @db.VarChar(255) + file_path String @db.VarChar(500) + file_type String? @db.VarChar(100) + file_size Int? + uploaded_by BigInt? + created_at DateTime @default(now()) @db.Timestamptz(6) + grn grn @relation(fields: [grn_id], references: [id], onDelete: Cascade, onUpdate: NoAction) + users users? @relation(fields: [uploaded_by], references: [id], onUpdate: NoAction) +} + +/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info. +model grn_items { + id BigInt @id @default(autoincrement()) + grn_id BigInt + po_item_id BigInt + item_id BigInt + line_no Int + ordered_qty Decimal @db.Decimal(15, 4) + previously_received_qty Decimal @default(0) @db.Decimal(15, 4) + current_qty Decimal @db.Decimal(15, 4) + accepted_qty Decimal @db.Decimal(15, 4) + rejected_qty Decimal @default(0) @db.Decimal(15, 4) + rejection_reason String? + uom_id BigInt + rate Decimal @db.Decimal(15, 4) + batch_no String? @db.VarChar(100) + mfg_date DateTime? @db.Date + expiry_date DateTime? @db.Date + storage_location String? @db.VarChar(100) + remarks String? + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) + assets assets[] + grn grn @relation(fields: [grn_id], references: [id], onDelete: Cascade, onUpdate: NoAction) + items items @relation(fields: [item_id], references: [id], onUpdate: NoAction) + purchase_order_items purchase_order_items @relation(fields: [po_item_id], references: [id], onUpdate: NoAction) + uom uom @relation(fields: [uom_id], references: [id], onUpdate: NoAction) + + @@unique([grn_id, line_no]) + @@index([grn_id], map: "idx_grn_items_grn_id") + @@index([item_id], map: "idx_grn_items_item_id") +} + +model gst_rates { + id BigInt @id @default(autoincrement()) + rate_pct Decimal @unique @db.Decimal(5, 2) + description String? @db.VarChar(100) + is_active Boolean @default(true) + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) + items items[] + purchase_order_items purchase_order_items[] +} + +model hsn_codes { + id BigInt @id @default(autoincrement()) + code String @unique @db.VarChar(20) + description String? + is_active Boolean @default(true) + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) + items items[] + purchase_order_items purchase_order_items[] +} + +model item_categories { + id BigInt @id @default(autoincrement()) + code String @unique @db.VarChar(30) + name String @db.VarChar(100) + is_active Boolean @default(true) + created_by BigInt? + updated_by BigInt? + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) + deleted_at DateTime? @db.Timestamptz(6) + users_item_categories_created_byTousers users? @relation("item_categories_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction) + users_item_categories_updated_byTousers users? @relation("item_categories_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction) + item_subcategories item_subcategories[] + items items[] +} + +model item_subcategories { + id BigInt @id @default(autoincrement()) + item_category_id BigInt + code String @unique @db.VarChar(30) + name String @db.VarChar(100) + is_active Boolean @default(true) + created_by BigInt? + updated_by BigInt? + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) + deleted_at DateTime? @db.Timestamptz(6) + users_item_subcategories_created_byTousers users? @relation("item_subcategories_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction) + item_categories item_categories @relation(fields: [item_category_id], references: [id], onUpdate: NoAction) + users_item_subcategories_updated_byTousers users? @relation("item_subcategories_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction) + items items[] +} + +model items { + id BigInt @id @default(autoincrement()) + item_code String @unique @db.VarChar(50) + item_name String @db.VarChar(200) + item_category_id BigInt + item_subcategory_id BigInt? + uom_id BigInt + hsn_code_id BigInt? + gst_rate_id BigInt? + brand_id BigInt? + is_asset_item Boolean @default(false) + description String? + specification String? + min_order_qty Decimal? @db.Decimal(15, 4) + reorder_level Decimal? @db.Decimal(15, 4) + is_active Boolean @default(true) + created_by BigInt? + updated_by BigInt? + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) + deleted_at DateTime? @db.Timestamptz(6) + grn_items grn_items[] + brands brands? @relation(fields: [brand_id], references: [id], onUpdate: NoAction) + users_items_created_byTousers users? @relation("items_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction) + gst_rates gst_rates? @relation(fields: [gst_rate_id], references: [id], onUpdate: NoAction) + hsn_codes hsn_codes? @relation(fields: [hsn_code_id], references: [id], onUpdate: NoAction) + item_categories item_categories @relation(fields: [item_category_id], references: [id], onUpdate: NoAction) + item_subcategories item_subcategories? @relation(fields: [item_subcategory_id], references: [id], onUpdate: NoAction) + uom uom @relation(fields: [uom_id], references: [id], onUpdate: NoAction) + users_items_updated_byTousers users? @relation("items_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction) + purchase_order_items purchase_order_items[] + vendor_item_mapping vendor_item_mapping[] + + @@index([item_category_id], map: "idx_items_category") + @@index([item_name(ops: raw("gin_trgm_ops"))], map: "idx_items_name_trgm", type: Gin) +} + +model modules { + id BigInt @id @default(autoincrement()) + code String @unique @db.VarChar(50) + name String @db.VarChar(100) + parent_module_id BigInt? + sort_order Int @default(0) + is_active Boolean @default(true) + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) + modules modules? @relation("modulesTomodules", fields: [parent_module_id], references: [id], onUpdate: NoAction) + other_modules modules[] @relation("modulesTomodules") + permissions permissions[] +} + +model password_reset_tokens { + id BigInt @id @default(autoincrement()) + user_id BigInt + token_hash String @unique @db.VarChar(64) + expires_at DateTime @db.Timestamptz(6) + used_at DateTime? @db.Timestamptz(6) + created_at DateTime @default(now()) @db.Timestamptz(6) + users users @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: NoAction) +} + +model payment_terms { + id BigInt @id @default(autoincrement()) + code String @unique @db.VarChar(30) + name String @db.VarChar(100) + credit_days Int @default(0) + description String? + is_active Boolean @default(true) + created_by BigInt? + updated_by BigInt? + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) + deleted_at DateTime? @db.Timestamptz(6) + users_payment_terms_created_byTousers users? @relation("payment_terms_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction) + users_payment_terms_updated_byTousers users? @relation("payment_terms_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction) + purchase_orders purchase_orders[] + vendors vendors[] +} + +model permissions { + id BigInt @id @default(autoincrement()) + module_id BigInt + action String @db.VarChar(50) + is_active Boolean @default(true) + created_at DateTime @default(now()) @db.Timestamptz(6) + modules modules @relation(fields: [module_id], references: [id], onDelete: Cascade, onUpdate: NoAction) + role_permissions role_permissions[] + + @@unique([module_id, action]) +} + +model plants { + id BigInt @id @default(autoincrement()) + code String @unique @db.VarChar(20) + name String @db.VarChar(150) + gstin String? @db.VarChar(15) + address String? + city String? @db.VarChar(100) + state String? @db.VarChar(100) + pincode String? @db.VarChar(10) + phone String? @db.VarChar(15) + is_active Boolean @default(true) + created_by BigInt? + updated_by BigInt? + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) + deleted_at DateTime? @db.Timestamptz(6) + asset_transfers_asset_transfers_from_plant_idToplants asset_transfers[] @relation("asset_transfers_from_plant_idToplants") + asset_transfers_asset_transfers_to_plant_idToplants asset_transfers[] @relation("asset_transfers_to_plant_idToplants") + assets assets[] + users_plants_created_byTousers users? @relation("plants_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction, map: "fk_plants_created_by") + users_plants_updated_byTousers users? @relation("plants_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction, map: "fk_plants_updated_by") + purchase_orders purchase_orders[] + users_users_plant_idToplants users[] @relation("users_plant_idToplants") + warehouses warehouses[] +} + +/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info. +model po_approvals { + id BigInt @id @default(autoincrement()) + po_id BigInt + approval_level Int + approver_role_id BigInt? + approver_user_id BigInt? + status String @default("PENDING") @db.VarChar(20) + remarks String? + acted_at DateTime? @db.Timestamptz(6) + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) + roles roles? @relation(fields: [approver_role_id], references: [id], onUpdate: NoAction) + users users? @relation(fields: [approver_user_id], references: [id], onUpdate: NoAction) + purchase_orders purchase_orders @relation(fields: [po_id], references: [id], onDelete: Cascade, onUpdate: NoAction) + + @@index([po_id], map: "idx_po_approvals_po_id") +} + +model po_attachments { + id BigInt @id @default(autoincrement()) + po_id BigInt + file_name String @db.VarChar(255) + file_path String @db.VarChar(500) + file_type String? @db.VarChar(100) + file_size Int? + uploaded_by BigInt? + created_at DateTime @default(now()) @db.Timestamptz(6) + purchase_orders purchase_orders @relation(fields: [po_id], references: [id], onDelete: Cascade, onUpdate: NoAction) + users users? @relation(fields: [uploaded_by], references: [id], onUpdate: NoAction) +} + +model purchase_order_items { + id BigInt @id @default(autoincrement()) + po_id BigInt + item_id BigInt + line_no Int + ordered_qty Decimal @db.Decimal(15, 4) + uom_id BigInt + rate Decimal @db.Decimal(15, 4) + discount_pct Decimal @default(0) @db.Decimal(5, 2) + discount_amount Decimal @default(0) @db.Decimal(15, 4) + gst_rate_id BigInt? + taxable_amount Decimal @default(0) @db.Decimal(15, 4) + tax_amount Decimal @default(0) @db.Decimal(15, 4) + line_total Decimal @default(0) @db.Decimal(15, 4) + received_qty Decimal @default(0) @db.Decimal(15, 4) + hsn_code_id BigInt? + remarks String? + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) + grn_items grn_items[] + gst_rates gst_rates? @relation(fields: [gst_rate_id], references: [id], onUpdate: NoAction) + hsn_codes hsn_codes? @relation(fields: [hsn_code_id], references: [id], onUpdate: NoAction) + items items @relation(fields: [item_id], references: [id], onUpdate: NoAction) + purchase_orders purchase_orders @relation(fields: [po_id], references: [id], onDelete: Cascade, onUpdate: NoAction) + uom uom @relation(fields: [uom_id], references: [id], onUpdate: NoAction) + + @@unique([po_id, line_no]) + @@index([item_id], map: "idx_po_items_item_id") + @@index([po_id], map: "idx_po_items_po_id") +} + +/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info. +model purchase_orders { + id BigInt @id @default(autoincrement()) + po_number String @unique @db.VarChar(30) + po_date DateTime @db.Date + po_type String @db.VarChar(30) + vendor_id BigInt + plant_id BigInt + warehouse_id BigInt? + brand_id BigInt? + payment_term_id BigInt? + delivery_term_id BigInt? + expected_delivery_date DateTime? @db.Date + sub_total Decimal @default(0) @db.Decimal(15, 4) + discount_amount Decimal @default(0) @db.Decimal(15, 4) + tax_total Decimal @default(0) @db.Decimal(15, 4) + freight_charges Decimal @default(0) @db.Decimal(15, 4) + other_charges Decimal @default(0) @db.Decimal(15, 4) + grand_total Decimal @default(0) @db.Decimal(15, 4) + status String @default("DRAFT") @db.VarChar(30) + revision_no Int @default(0) + parent_po_id BigInt? + terms_and_conditions String? + remarks String? + is_active Boolean @default(true) + created_by BigInt? + updated_by BigInt? + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) + deleted_at DateTime? @db.Timestamptz(6) + assets assets[] + grn grn[] + po_approvals po_approvals[] + po_attachments po_attachments[] + purchase_order_items purchase_order_items[] + brands brands? @relation(fields: [brand_id], references: [id], onUpdate: NoAction) + users_purchase_orders_created_byTousers users? @relation("purchase_orders_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction) + delivery_terms delivery_terms? @relation(fields: [delivery_term_id], references: [id], onUpdate: NoAction) + purchase_orders purchase_orders? @relation("purchase_ordersTopurchase_orders", fields: [parent_po_id], references: [id], onUpdate: NoAction) + other_purchase_orders purchase_orders[] @relation("purchase_ordersTopurchase_orders") + payment_terms payment_terms? @relation(fields: [payment_term_id], references: [id], onUpdate: NoAction) + plants plants @relation(fields: [plant_id], references: [id], onUpdate: NoAction) + users_purchase_orders_updated_byTousers users? @relation("purchase_orders_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction) + vendors vendors @relation(fields: [vendor_id], references: [id], onUpdate: NoAction) + warehouses warehouses? @relation(fields: [warehouse_id], references: [id], onDelete: Restrict, onUpdate: NoAction) + + @@index([created_at], map: "idx_po_created_at") + @@index([po_date], map: "idx_po_po_date") + @@index([vendor_id], map: "idx_po_vendor_id") +} + +model refresh_tokens { + id BigInt @id @default(autoincrement()) + user_id BigInt + token_hash String @unique @db.VarChar(64) + expires_at DateTime @db.Timestamptz(6) + revoked_at DateTime? @db.Timestamptz(6) + created_at DateTime @default(now()) @db.Timestamptz(6) + users users @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: NoAction) + + @@index([user_id], map: "idx_refresh_tokens_user_id") +} + +model role_permissions { + id BigInt @id @default(autoincrement()) + role_id BigInt + permission_id BigInt + created_at DateTime @default(now()) @db.Timestamptz(6) + permissions permissions @relation(fields: [permission_id], references: [id], onDelete: Cascade, onUpdate: NoAction) + roles roles @relation(fields: [role_id], references: [id], onDelete: Cascade, onUpdate: NoAction) + + @@unique([role_id, permission_id]) +} + +model roles { + id BigInt @id @default(autoincrement()) + name String @unique @db.VarChar(100) + description String? + is_active Boolean @default(true) + created_by BigInt? + updated_by BigInt? + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) + deleted_at DateTime? @db.Timestamptz(6) + po_approvals po_approvals[] + role_permissions role_permissions[] + users_roles_created_byTousers users? @relation("roles_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction, map: "fk_roles_created_by") + users_roles_updated_byTousers users? @relation("roles_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction, map: "fk_roles_updated_by") + users_users_role_idToroles users[] @relation("users_role_idToroles") +} + +model uom { + id BigInt @id @default(autoincrement()) + code String @unique @db.VarChar(20) + name String @db.VarChar(100) + is_active Boolean @default(true) + created_by BigInt? + updated_by BigInt? + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) + deleted_at DateTime? @db.Timestamptz(6) + grn_items grn_items[] + items items[] + purchase_order_items purchase_order_items[] + users_uom_created_byTousers users? @relation("uom_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction) + users_uom_updated_byTousers users? @relation("uom_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction) +} + +/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info. +model users { + id BigInt @id @default(autoincrement()) + employee_code String @unique @db.VarChar(30) + full_name String @db.VarChar(200) + email String @unique @db.VarChar(200) + mobile String? @db.VarChar(20) + password_hash String @db.VarChar(255) + role_id BigInt? + department_id BigInt? + designation_id BigInt? + plant_id BigInt? + reporting_to BigInt? + status String @default("active") @db.VarChar(20) + is_active Boolean @default(true) + failed_login_attempts Int @default(0) + locked_until DateTime? @db.Timestamptz(6) + last_login_at DateTime? @db.Timestamptz(6) + created_by BigInt? + updated_by BigInt? + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) + deleted_at DateTime? @db.Timestamptz(6) + asset_attachments asset_attachments[] + asset_categories_asset_categories_created_byTousers asset_categories[] @relation("asset_categories_created_byTousers") + asset_categories_asset_categories_updated_byTousers asset_categories[] @relation("asset_categories_updated_byTousers") + asset_transfers_asset_transfers_from_user_idTousers asset_transfers[] @relation("asset_transfers_from_user_idTousers") + asset_transfers_asset_transfers_to_user_idTousers asset_transfers[] @relation("asset_transfers_to_user_idTousers") + asset_transfers_asset_transfers_transferred_byTousers asset_transfers[] @relation("asset_transfers_transferred_byTousers") + assets_assets_assigned_to_user_idTousers assets[] @relation("assets_assigned_to_user_idTousers") + assets_assets_created_byTousers assets[] @relation("assets_created_byTousers") + assets_assets_updated_byTousers assets[] @relation("assets_updated_byTousers") + audit_logs audit_logs[] + brands_brands_created_byTousers brands[] @relation("brands_created_byTousers") + brands_brands_updated_byTousers brands[] @relation("brands_updated_byTousers") + delivery_terms_delivery_terms_created_byTousers delivery_terms[] @relation("delivery_terms_created_byTousers") + delivery_terms_delivery_terms_updated_byTousers delivery_terms[] @relation("delivery_terms_updated_byTousers") + document_series_document_series_created_byTousers document_series[] @relation("document_series_created_byTousers") + document_series_document_series_updated_byTousers document_series[] @relation("document_series_updated_byTousers") + grn_grn_cancelled_byTousers grn[] @relation("grn_cancelled_byTousers") + grn_grn_created_byTousers grn[] @relation("grn_created_byTousers") + grn_grn_quality_checked_byTousers grn[] @relation("grn_quality_checked_byTousers") + grn_grn_received_byTousers grn[] @relation("grn_received_byTousers") + grn_grn_updated_byTousers grn[] @relation("grn_updated_byTousers") + grn_attachments grn_attachments[] + item_categories_item_categories_created_byTousers item_categories[] @relation("item_categories_created_byTousers") + item_categories_item_categories_updated_byTousers item_categories[] @relation("item_categories_updated_byTousers") + item_subcategories_item_subcategories_created_byTousers item_subcategories[] @relation("item_subcategories_created_byTousers") + item_subcategories_item_subcategories_updated_byTousers item_subcategories[] @relation("item_subcategories_updated_byTousers") + items_items_created_byTousers items[] @relation("items_created_byTousers") + items_items_updated_byTousers items[] @relation("items_updated_byTousers") + password_reset_tokens password_reset_tokens[] + payment_terms_payment_terms_created_byTousers payment_terms[] @relation("payment_terms_created_byTousers") + payment_terms_payment_terms_updated_byTousers payment_terms[] @relation("payment_terms_updated_byTousers") + plants_plants_created_byTousers plants[] @relation("plants_created_byTousers") + plants_plants_updated_byTousers plants[] @relation("plants_updated_byTousers") + po_approvals po_approvals[] + po_attachments po_attachments[] + purchase_orders_purchase_orders_created_byTousers purchase_orders[] @relation("purchase_orders_created_byTousers") + purchase_orders_purchase_orders_updated_byTousers purchase_orders[] @relation("purchase_orders_updated_byTousers") + refresh_tokens refresh_tokens[] + roles_roles_created_byTousers roles[] @relation("roles_created_byTousers") + roles_roles_updated_byTousers roles[] @relation("roles_updated_byTousers") + uom_uom_created_byTousers uom[] @relation("uom_created_byTousers") + uom_uom_updated_byTousers uom[] @relation("uom_updated_byTousers") + users_users_created_byTousers users? @relation("users_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction) + other_users_users_created_byTousers users[] @relation("users_created_byTousers") + departments departments? @relation(fields: [department_id], references: [id], onUpdate: NoAction) + designations designations? @relation(fields: [designation_id], references: [id], onUpdate: NoAction) + plants_users_plant_idToplants plants? @relation("users_plant_idToplants", fields: [plant_id], references: [id], onUpdate: NoAction) + users_users_reporting_toTousers users? @relation("users_reporting_toTousers", fields: [reporting_to], references: [id], onUpdate: NoAction) + other_users_users_reporting_toTousers users[] @relation("users_reporting_toTousers") + roles_users_role_idToroles roles? @relation("users_role_idToroles", fields: [role_id], references: [id], onDelete: Restrict, onUpdate: NoAction) + users_users_updated_byTousers users? @relation("users_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction) + other_users_users_updated_byTousers users[] @relation("users_updated_byTousers") + vendor_addresses_vendor_addresses_created_byTousers vendor_addresses[] @relation("vendor_addresses_created_byTousers") + vendor_addresses_vendor_addresses_updated_byTousers vendor_addresses[] @relation("vendor_addresses_updated_byTousers") + vendor_bank_details_vendor_bank_details_created_byTousers vendor_bank_details[] @relation("vendor_bank_details_created_byTousers") + vendor_bank_details_vendor_bank_details_updated_byTousers vendor_bank_details[] @relation("vendor_bank_details_updated_byTousers") + vendor_contacts_vendor_contacts_created_byTousers vendor_contacts[] @relation("vendor_contacts_created_byTousers") + vendor_contacts_vendor_contacts_updated_byTousers vendor_contacts[] @relation("vendor_contacts_updated_byTousers") + vendor_item_mapping_vendor_item_mapping_created_byTousers vendor_item_mapping[] @relation("vendor_item_mapping_created_byTousers") + vendor_item_mapping_vendor_item_mapping_updated_byTousers vendor_item_mapping[] @relation("vendor_item_mapping_updated_byTousers") + vendors_vendors_created_byTousers vendors[] @relation("vendors_created_byTousers") + vendors_vendors_updated_byTousers vendors[] @relation("vendors_updated_byTousers") + warehouses_warehouses_created_byTousers warehouses[] @relation("warehouses_created_byTousers") + warehouses_warehouses_updated_byTousers warehouses[] @relation("warehouses_updated_byTousers") + + @@index([role_id], map: "idx_users_role_id") +} + +/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info. +model vendor_addresses { + id BigInt @id @default(autoincrement()) + vendor_id BigInt + address_type String @db.VarChar(20) + address_line1 String? @db.VarChar(200) + address_line2 String? @db.VarChar(200) + city String? @db.VarChar(100) + state String? @db.VarChar(100) + pincode String? @db.VarChar(10) + country String @default("India") @db.VarChar(100) + gstin String? @db.VarChar(15) + is_active Boolean @default(true) + created_by BigInt? + updated_by BigInt? + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) + users_vendor_addresses_created_byTousers users? @relation("vendor_addresses_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction) + users_vendor_addresses_updated_byTousers users? @relation("vendor_addresses_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction) + vendors vendors @relation(fields: [vendor_id], references: [id], onDelete: Cascade, onUpdate: NoAction) +} + +/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info. +model vendor_bank_details { + id BigInt @id @default(autoincrement()) + vendor_id BigInt + bank_name String @db.VarChar(150) + branch String? @db.VarChar(150) + account_number String @db.VarChar(255) + account_number_index String? @db.VarChar(64) + ifsc String @db.VarChar(15) + account_holder_name String @db.VarChar(200) + account_type String @default("CURRENT") @db.VarChar(20) + is_primary Boolean @default(false) + is_active Boolean @default(true) + created_by BigInt? + updated_by BigInt? + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) + users_vendor_bank_details_created_byTousers users? @relation("vendor_bank_details_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction) + users_vendor_bank_details_updated_byTousers users? @relation("vendor_bank_details_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction) + vendors vendors @relation(fields: [vendor_id], references: [id], onDelete: Cascade, onUpdate: NoAction) +} + +model vendor_contacts { + id BigInt @id @default(autoincrement()) + vendor_id BigInt + contact_name String @db.VarChar(150) + designation String? @db.VarChar(100) + phone String? @db.VarChar(15) + email String? @db.VarChar(150) + is_primary Boolean @default(false) + is_active Boolean @default(true) + created_by BigInt? + updated_by BigInt? + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) + users_vendor_contacts_created_byTousers users? @relation("vendor_contacts_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction) + users_vendor_contacts_updated_byTousers users? @relation("vendor_contacts_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction) + vendors vendors @relation(fields: [vendor_id], references: [id], onDelete: Cascade, onUpdate: NoAction) +} + +model vendor_item_mapping { + id BigInt @id @default(autoincrement()) + vendor_id BigInt + item_id BigInt + last_purchase_rate Decimal? @db.Decimal(15, 4) + is_preferred Boolean @default(false) + is_active Boolean @default(true) + created_by BigInt? + updated_by BigInt? + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) + users_vendor_item_mapping_created_byTousers users? @relation("vendor_item_mapping_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction) + items items @relation(fields: [item_id], references: [id], onDelete: Cascade, onUpdate: NoAction) + users_vendor_item_mapping_updated_byTousers users? @relation("vendor_item_mapping_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction) + vendors vendors @relation(fields: [vendor_id], references: [id], onDelete: Cascade, onUpdate: NoAction) + + @@unique([vendor_id, item_id]) +} + +/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info. +model vendors { + id BigInt @id @default(autoincrement()) + vendor_code String @unique @db.VarChar(30) + vendor_name String @db.VarChar(200) + vendor_type String @db.VarChar(30) + gstin String? @db.VarChar(15) + pan String? @db.VarChar(10) + payment_term_id BigInt? + credit_period_days Int @default(0) + status String @default("active") @db.VarChar(20) + remarks String? + is_active Boolean @default(true) + created_by BigInt? + updated_by BigInt? + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) + deleted_at DateTime? @db.Timestamptz(6) + assets_assets_amc_vendor_idTovendors assets[] @relation("assets_amc_vendor_idTovendors") + assets_assets_vendor_idTovendors assets[] @relation("assets_vendor_idTovendors") + grn grn[] + purchase_orders purchase_orders[] + vendor_addresses vendor_addresses[] + vendor_bank_details vendor_bank_details[] + vendor_contacts vendor_contacts[] + vendor_item_mapping vendor_item_mapping[] + users_vendors_created_byTousers users? @relation("vendors_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction) + payment_terms payment_terms? @relation(fields: [payment_term_id], references: [id], onUpdate: NoAction) + users_vendors_updated_byTousers users? @relation("vendors_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction) + + @@index([vendor_name(ops: raw("gin_trgm_ops"))], map: "idx_vendors_name_trgm", type: Gin) +} + +model warehouses { + id BigInt @id @default(autoincrement()) + code String @unique @db.VarChar(30) + name String @db.VarChar(150) + plant_id BigInt + location String? @db.VarChar(200) + is_active Boolean @default(true) + created_by BigInt? + updated_by BigInt? + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) + deleted_at DateTime? @db.Timestamptz(6) + asset_transfers_asset_transfers_from_warehouse_idTowarehouses asset_transfers[] @relation("asset_transfers_from_warehouse_idTowarehouses") + asset_transfers_asset_transfers_to_warehouse_idTowarehouses asset_transfers[] @relation("asset_transfers_to_warehouse_idTowarehouses") + assets assets[] + grn grn[] + purchase_orders purchase_orders[] + users_warehouses_created_byTousers users? @relation("warehouses_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction) + plants plants @relation(fields: [plant_id], references: [id], onUpdate: NoAction) + users_warehouses_updated_byTousers users? @relation("warehouses_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction) +} diff --git a/prisma/seed.js b/prisma/seed.js new file mode 100644 index 0000000..6f60223 --- /dev/null +++ b/prisma/seed.js @@ -0,0 +1,49 @@ +/* eslint-disable no-console */ +const { PrismaClient } = require('@prisma/client'); +const bcrypt = require('bcrypt'); + +const prisma = new PrismaClient(); + +async function main() { + const superAdminRole = await prisma.roles.findFirst({ + where: { name: 'Super Admin', deleted_at: null }, + }); + + if (!superAdminRole) { + throw new Error('Super Admin role not found. Run erp_phase1_ddl.sql first.'); + } + + const passwordHash = await bcrypt.hash('Admin@123', 12); + + await prisma.users.upsert({ + where: { email: 'admin@bharaterp.com' }, + update: { + role_id: superAdminRole.id, + status: 'active', + is_active: true, + deleted_at: null, + }, + create: { + employee_code: 'EMP001', + full_name: 'Super Admin', + email: 'admin@bharaterp.com', + password_hash: passwordHash, + role_id: superAdminRole.id, + status: 'active', + is_active: true, + }, + }); + + console.log('Bootstrap Super Admin ready: admin@bharaterp.com / Admin@123'); +} + +main() + .then(async () => { + await prisma.$disconnect(); + process.exit(0); + }) + .catch(async (err) => { + console.error('Seed failed', err); + await prisma.$disconnect(); + process.exit(1); + }); diff --git a/scripts/setup-db.sh b/scripts/setup-db.sh new file mode 100755 index 0000000..fa4283c --- /dev/null +++ b/scripts/setup-db.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -euo pipefail + +DB_HOST="${DB_HOST:-demo.venbait.in}" +DB_PORT="${DB_PORT:-5432}" +DB_NAME="${DB_NAME:-bharaterp}" +DB_USER="${DB_USER:-bharaterpdevdbuser}" +DB_PASSWORD="${DB_PASSWORD:-devdbuser@bharaterp}" +DDL_PATH="${1:-/home/smart/Downloads/erp_phase1_ddl.sql}" +PRISMA_VERSION="${PRISMA_VERSION:-5.22.0}" + +if [ ! -f "$DDL_PATH" ]; then + echo "DDL file not found: $DDL_PATH" + exit 1 +fi + +ENCODED_PASSWORD="${DB_PASSWORD//@/%40}" +POSTGRES_URL="postgresql://${DB_USER}:${ENCODED_PASSWORD}@${DB_HOST}:${DB_PORT}/postgres?schema=public" +TARGET_URL="postgresql://${DB_USER}:${ENCODED_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?schema=public" + +echo "Checking Prisma CLI availability (npx prisma@${PRISMA_VERSION})" +npx "prisma@${PRISMA_VERSION}" --version >/dev/null + +echo "Trying to create database: $DB_NAME" +set +e +DATABASE_URL="$POSTGRES_URL" npx "prisma@${PRISMA_VERSION}" db execute --stdin --schema prisma/schema.prisma < o.trim()) || ['*'] })); +app.use(hpp()); +app.use(compression()); +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true })); +app.use(requestId); +app.use(morganMiddleware); + +app.get('/health', (req, res) => { + res.json({ success: true, message: 'OK', uptime: process.uptime() }); +}); + +app.use('/api', generalLimiter); +app.use('/api/v1', routesV1); + +app.get('/api-docs.json', (_req, res) => { + res.setHeader('Cache-Control', 'no-store'); + res.json(swaggerSpec); +}); + +app.use( + '/api-docs', + helmet({ contentSecurityPolicy: false }), + swaggerUi.serve, + swaggerUi.setup(null, { + explorer: true, + swaggerUrl: '/api-docs.json', + swaggerOptions: { + persistAuthorization: true, + }, + }) +); + +app.use((req, _res, next) => next(new ApiError(404, `Route not found: ${req.originalUrl}`))); +app.use(errorMiddleware); + +module.exports = app; diff --git a/src/config/env.js b/src/config/env.js new file mode 100644 index 0000000..b6d16b1 --- /dev/null +++ b/src/config/env.js @@ -0,0 +1,45 @@ +const path = require('path'); +const dotenv = require('dotenv'); +const Joi = require('joi'); + +dotenv.config({ path: path.resolve(process.cwd(), '.env') }); + +const envSchema = Joi.object({ + NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'), + PORT: Joi.number().default(3000), + + DATABASE_URL: Joi.string().required(), + DB_HOST: Joi.string().required(), + DB_PORT: Joi.number().default(5432), + DB_NAME: Joi.string().required(), + DB_USER: Joi.string().required(), + DB_PASSWORD: Joi.string().required(), + + JWT_ACCESS_SECRET: Joi.string().min(32).required(), + JWT_ACCESS_EXPIRY: Joi.string().default('15m'), + JWT_REFRESH_SECRET: Joi.string().min(32).required(), + JWT_REFRESH_EXPIRY: Joi.string().default('7d'), + + BCRYPT_SALT_ROUNDS: Joi.number().default(12), + ENCRYPTION_KEY: Joi.string().length(64).required(), + ENCRYPTION_HMAC_KEY: Joi.string().min(32).required(), + + CORS_ORIGINS: Joi.string().default('*'), + RATE_LIMIT_WINDOW_MS: Joi.number().default(900000), + RATE_LIMIT_MAX: Joi.number().default(100), + AUTH_RATE_LIMIT_MAX: Joi.number().default(10), + + LOG_LEVEL: Joi.string().default('info'), + UPLOAD_DIR: Joi.string().default('uploads'), + MAX_FILE_SIZE_MB: Joi.number().default(5), + + MAX_LOGIN_ATTEMPTS: Joi.number().default(5), + LOCKOUT_DURATION_MINUTES: Joi.number().default(30), +}).unknown(); + +const { error, value: env } = envSchema.validate(process.env); +if (error) { + throw new Error(`Environment validation error: ${error.message}`); +} + +module.exports = env; diff --git a/src/config/logger.js b/src/config/logger.js new file mode 100644 index 0000000..2e9f73b --- /dev/null +++ b/src/config/logger.js @@ -0,0 +1,53 @@ +const winston = require('winston'); +require('winston-daily-rotate-file'); +const path = require('path'); +const env = require('./env'); + +const { combine, timestamp, printf, errors, json } = winston.format; +const SENSITIVE_KEYS = ['password', 'token', 'authorization', 'refresh_token', 'access_token']; + +const redact = winston.format((info) => { + const scrub = (obj) => { + if (!obj || typeof obj !== 'object') return obj; + const out = Array.isArray(obj) ? [] : {}; + for (const [k, v] of Object.entries(obj)) { + out[k] = SENSITIVE_KEYS.includes(k.toLowerCase()) ? '[REDACTED]' : scrub(v); + } + return out; + }; + return scrub(info); +}); + +const devFormat = combine( + redact(), + timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + errors({ stack: true }), + printf(({ level, message, timestamp: ts, stack, ...meta }) => { + const metaStr = Object.keys(meta).length ? JSON.stringify(meta) : ''; + return `${ts} [${level}]: ${stack || message} ${metaStr}`; + }) +); + +const prodFormat = combine(redact(), timestamp(), errors({ stack: true }), json()); + +const logger = winston.createLogger({ + level: env.LOG_LEVEL, + format: env.NODE_ENV === 'production' ? prodFormat : devFormat, + transports: [ + new winston.transports.Console(), + new winston.transports.DailyRotateFile({ + filename: path.join('logs', 'error-%DATE%.log'), + datePattern: 'YYYY-MM-DD', + level: 'error', + maxFiles: '30d', + }), + new winston.transports.DailyRotateFile({ + filename: path.join('logs', 'combined-%DATE%.log'), + datePattern: 'YYYY-MM-DD', + maxFiles: '30d', + }), + ], + exitOnError: false, +}); + +module.exports = logger; diff --git a/src/config/morgan.js b/src/config/morgan.js new file mode 100644 index 0000000..bb99c2d --- /dev/null +++ b/src/config/morgan.js @@ -0,0 +1,12 @@ +const morgan = require('morgan'); +const logger = require('./logger'); + +morgan.token('id', (req) => req.id); +morgan.token('user', (req) => (req.user ? req.user.id : 'anonymous')); + +const format = + ':id :remote-addr :method :url :status :res[content-length]B - :response-time ms user=:user'; + +module.exports = morgan(format, { + stream: { write: (message) => logger.http(message.trim()) }, +}); diff --git a/src/config/prisma.js b/src/config/prisma.js new file mode 100644 index 0000000..4ba9cea --- /dev/null +++ b/src/config/prisma.js @@ -0,0 +1,21 @@ +const { PrismaClient } = require('@prisma/client'); +const logger = require('./logger'); +const env = require('./env'); + +const prisma = new PrismaClient({ + log: [ + { emit: 'event', level: 'error' }, + { emit: 'event', level: 'warn' }, + ...(env.NODE_ENV === 'development' ? [{ emit: 'event', level: 'query' }] : []), + ], +}); + +prisma.$on('error', (e) => logger.error('Prisma error', { error: e.message })); +prisma.$on('warn', (e) => logger.warn('Prisma warning', { warning: e.message })); +if (env.NODE_ENV === 'development') { + prisma.$on('query', (e) => + logger.debug('Prisma query', { query: e.query, duration: e.duration }) + ); +} + +module.exports = prisma; diff --git a/src/config/swagger.js b/src/config/swagger.js new file mode 100644 index 0000000..ec5c891 --- /dev/null +++ b/src/config/swagger.js @@ -0,0 +1,45 @@ +const fs = require('fs'); +const path = require('path'); +const yaml = require('yaml'); + +const definition = { + openapi: '3.0.0', + info: { + title: 'ERP API', + version: '1.0.0', + description: 'Phase 1 - PO, GRN, Vendor, Assets, Masters, Users & RBAC', + }, + servers: [{ url: '/api/v1' }], + components: { + securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' } }, + schemas: {}, + }, + security: [{ bearerAuth: [] }], + tags: [], + paths: {}, +}; + +const docsDir = path.join(__dirname, '../docs'); + +if (fs.existsSync(docsDir)) { + for (const file of fs.readdirSync(docsDir)) { + if (!/\.ya?ml$/i.test(file)) continue; + + const doc = yaml.parse(fs.readFileSync(path.join(docsDir, file), 'utf8')); + if (!doc) continue; + + if (doc.tags?.length) { + definition.tags.push(...doc.tags); + } + + if (doc.components?.schemas) { + Object.assign(definition.components.schemas, doc.components.schemas); + } + + if (doc.paths) { + Object.assign(definition.paths, doc.paths); + } + } +} + +module.exports = definition; diff --git a/src/docs/completed-routes.yaml b/src/docs/completed-routes.yaml new file mode 100644 index 0000000..9dabdf3 --- /dev/null +++ b/src/docs/completed-routes.yaml @@ -0,0 +1,1641 @@ +tags: + - name: Auth + - name: UOM + - name: Item Categories + - name: Item Subcategories + - name: Items + - name: Brands + - name: GST Rates + - name: Payment Terms + - name: Delivery Terms + - name: Asset Categories + - name: Departments + - name: Designations + - name: Warehouses + - name: Plants + - name: Document Series + - name: Users + - name: Roles + +components: + schemas: + ApiResponse: + type: object + properties: + success: { type: boolean } + message: { type: string } + data: { type: object, nullable: true } + meta: { type: object, nullable: true } + errors: { type: array, items: { type: object } } + AuthLoginBody: + type: object + required: [email, password] + properties: + email: { type: string, format: email, example: "admin@bharaterp.com" } + password: { type: string, minLength: 8, example: "Admin@123" } + AuthRefreshBody: + type: object + required: [refresh_token] + properties: + refresh_token: { type: string, example: "" } + UomCreateBody: + type: object + required: [code, name] + properties: + code: { type: string, example: 'KG' } + name: { type: string, example: 'Kilogram' } + is_active: { type: boolean, example: true } + UomUpdateBody: + type: object + minProperties: 1 + properties: + code: { type: string, example: 'KG' } + name: { type: string, example: 'Kilogram' } + is_active: { type: boolean, example: true } + ItemCategoriesCreateBody: + type: object + required: [code, name] + properties: + code: { type: string, example: 'RAW' } + name: { type: string, example: 'Raw Material' } + is_active: { type: boolean, example: true } + ItemCategoriesUpdateBody: + type: object + minProperties: 1 + properties: + code: { type: string, example: 'RAW' } + name: { type: string, example: 'Raw Material' } + is_active: { type: boolean, example: true } + ItemSubcategoriesCreateBody: + type: object + required: [item_category_id, code, name] + properties: + item_category_id: { type: integer, example: 1 } + code: { type: string, example: 'CHEM' } + name: { type: string, example: 'Chemicals' } + is_active: { type: boolean, example: true } + ItemSubcategoriesUpdateBody: + type: object + minProperties: 1 + properties: + item_category_id: { type: integer, example: 1 } + code: { type: string, example: 'CHEM' } + name: { type: string, example: 'Chemicals' } + is_active: { type: boolean, example: true } + ItemsCreateBody: + type: object + required: [item_code, item_name, item_category_id, uom_id, is_asset_item, min_order_qty, reorder_level] + properties: + item_code: { type: string, example: 'RM-LABSA' } + item_name: { type: string, example: 'LABSA' } + item_category_id: { type: integer, example: 1 } + item_subcategory_id: { type: integer, example: 1 } + uom_id: { type: integer, example: 1 } + hsn_code_id: { type: integer, example: 1 } + gst_rate_id: { type: integer, example: 4 } + brand_id: { type: integer, example: 1 } + is_asset_item: { type: boolean, example: false } + description: { type: string, example: 'Raw material' } + specification: { type: string, example: '96% purity' } + min_order_qty: { type: number, example: 100.0 } + reorder_level: { type: number, example: 500.0 } + is_active: { type: boolean, example: true } + ItemsUpdateBody: + type: object + minProperties: 1 + properties: + item_code: { type: string, example: 'RM-LABSA' } + item_name: { type: string, example: 'LABSA' } + item_category_id: { type: integer, example: 1 } + item_subcategory_id: { type: integer, example: 1 } + uom_id: { type: integer, example: 1 } + hsn_code_id: { type: integer, example: 1 } + gst_rate_id: { type: integer, example: 4 } + brand_id: { type: integer, example: 1 } + is_asset_item: { type: boolean, example: false } + description: { type: string, example: 'Raw material' } + specification: { type: string, example: '96% purity' } + min_order_qty: { type: number, example: 100.0 } + reorder_level: { type: number, example: 500.0 } + is_active: { type: boolean, example: true } + BrandsCreateBody: + type: object + required: [code, name, brand_type] + properties: + code: { type: string, example: 'OWN01' } + name: { type: string, example: 'Bharat Clean' } + brand_type: { type: string, example: 'OWN' } + contact_person: { type: string, example: 'Ravi' } + phone: { type: string, example: '9876543210' } + email: { type: string, example: 'ops@bharaterp.com' } + is_active: { type: boolean, example: true } + BrandsUpdateBody: + type: object + minProperties: 1 + properties: + code: { type: string, example: 'OWN01' } + name: { type: string, example: 'Bharat Clean' } + brand_type: { type: string, example: 'OWN' } + contact_person: { type: string, example: 'Ravi' } + phone: { type: string, example: '9876543210' } + email: { type: string, example: 'ops@bharaterp.com' } + is_active: { type: boolean, example: true } + GstRatesCreateBody: + type: object + required: [rate_pct] + properties: + rate_pct: { type: number, example: 18 } + description: { type: string, example: 'GST 18%' } + is_active: { type: boolean, example: true } + GstRatesUpdateBody: + type: object + minProperties: 1 + properties: + rate_pct: { type: number, example: 18 } + description: { type: string, example: 'GST 18%' } + is_active: { type: boolean, example: true } + PaymentTermsCreateBody: + type: object + required: [code, name, credit_days] + properties: + code: { type: string, example: 'NET30' } + name: { type: string, example: 'Net 30 Days' } + credit_days: { type: integer, example: 30 } + description: { type: string, example: 'Standard credit' } + is_active: { type: boolean, example: true } + PaymentTermsUpdateBody: + type: object + minProperties: 1 + properties: + code: { type: string, example: 'NET30' } + name: { type: string, example: 'Net 30 Days' } + credit_days: { type: integer, example: 30 } + description: { type: string, example: 'Standard credit' } + is_active: { type: boolean, example: true } + DeliveryTermsCreateBody: + type: object + required: [code, name] + properties: + code: { type: string, example: 'FOR' } + name: { type: string, example: 'Free on Road' } + description: { type: string, example: 'Delivered to site' } + is_active: { type: boolean, example: true } + DeliveryTermsUpdateBody: + type: object + minProperties: 1 + properties: + code: { type: string, example: 'FOR' } + name: { type: string, example: 'Free on Road' } + description: { type: string, example: 'Delivered to site' } + is_active: { type: boolean, example: true } + AssetCategoriesCreateBody: + type: object + required: [code, name, code_prefix, default_useful_life_years, default_depreciation_method] + properties: + code: { type: string, example: 'MCH' } + name: { type: string, example: 'Plant & Machinery' } + code_prefix: { type: string, example: 'MCH' } + default_useful_life_years: { type: integer, example: 15 } + default_depreciation_method: { type: string, example: 'WDV' } + is_active: { type: boolean, example: true } + AssetCategoriesUpdateBody: + type: object + minProperties: 1 + properties: + code: { type: string, example: 'MCH' } + name: { type: string, example: 'Plant & Machinery' } + code_prefix: { type: string, example: 'MCH' } + default_useful_life_years: { type: integer, example: 15 } + default_depreciation_method: { type: string, example: 'WDV' } + is_active: { type: boolean, example: true } + DepartmentsCreateBody: + type: object + required: [name] + properties: + name: { type: string, example: 'Procurement' } + is_active: { type: boolean, example: true } + DepartmentsUpdateBody: + type: object + minProperties: 1 + properties: + name: { type: string, example: 'Procurement' } + is_active: { type: boolean, example: true } + DesignationsCreateBody: + type: object + required: [name] + properties: + name: { type: string, example: 'Store Manager' } + is_active: { type: boolean, example: true } + DesignationsUpdateBody: + type: object + minProperties: 1 + properties: + name: { type: string, example: 'Store Manager' } + is_active: { type: boolean, example: true } + WarehousesCreateBody: + type: object + required: [code, name, plant_id] + properties: + code: { type: string, example: 'WH01' } + name: { type: string, example: 'Main Warehouse' } + plant_id: { type: integer, example: 1 } + location: { type: string, example: 'Ground Floor' } + is_active: { type: boolean, example: true } + WarehousesUpdateBody: + type: object + minProperties: 1 + properties: + code: { type: string, example: 'WH01' } + name: { type: string, example: 'Main Warehouse' } + plant_id: { type: integer, example: 1 } + location: { type: string, example: 'Ground Floor' } + is_active: { type: boolean, example: true } + PlantsCreateBody: + type: object + required: [code, name] + properties: + code: { type: string, example: 'PLT01' } + name: { type: string, example: 'Bharat Plant 1' } + gstin: { type: string, example: '27ABCDE1234F1Z5' } + address: { type: string, example: 'MIDC, Nashik' } + city: { type: string, example: 'Nashik' } + state: { type: string, example: 'Maharashtra' } + pincode: { type: string, example: '422001' } + phone: { type: string, example: '0253-1234567' } + is_active: { type: boolean, example: true } + PlantsUpdateBody: + type: object + minProperties: 1 + properties: + code: { type: string, example: 'PLT01' } + name: { type: string, example: 'Bharat Plant 1' } + gstin: { type: string, example: '27ABCDE1234F1Z5' } + address: { type: string, example: 'MIDC, Nashik' } + city: { type: string, example: 'Nashik' } + state: { type: string, example: 'Maharashtra' } + pincode: { type: string, example: '422001' } + phone: { type: string, example: '0253-1234567' } + is_active: { type: boolean, example: true } + DocumentSeriesCreateBody: + type: object + required: [code, prefix, current_number, padding] + properties: + code: { type: string, example: 'PO' } + prefix: { type: string, example: 'PO/2026-27/' } + current_number: { type: integer, example: 0 } + padding: { type: integer, example: 5 } + description: { type: string, example: 'PO Number Series' } + is_active: { type: boolean, example: true } + DocumentSeriesUpdateBody: + type: object + minProperties: 1 + properties: + code: { type: string, example: 'PO' } + prefix: { type: string, example: 'PO/2026-27/' } + current_number: { type: integer, example: 0 } + padding: { type: integer, example: 5 } + description: { type: string, example: 'PO Number Series' } + is_active: { type: boolean, example: true } + + UsersCreateBody: + type: object + required: [employee_code, full_name, email, password, role_id] + properties: + employee_code: { type: string, example: 'EMP002' } + full_name: { type: string, example: 'Jane Doe' } + email: { type: string, format: email, example: 'jane@bharaterp.com' } + password: { type: string, minLength: 8, example: 'Admin@123' } + mobile: { type: string, example: '9876543210' } + role_id: { type: integer, example: 2 } + department_id: { type: integer, example: 1 } + designation_id: { type: integer, example: 1 } + plant_id: { type: integer, example: 1 } + reporting_to: { type: integer, example: 1 } + status: { type: string, enum: [active, inactive, locked], example: active } + is_active: { type: boolean, example: true } + UsersUpdateBody: + type: object + minProperties: 1 + properties: + employee_code: { type: string, example: 'EMP002' } + full_name: { type: string, example: 'Jane Doe' } + email: { type: string, format: email, example: 'jane@bharaterp.com' } + password: { type: string, minLength: 8, example: 'NewPass@123' } + mobile: { type: string, example: '9876543210' } + role_id: { type: integer, example: 2 } + department_id: { type: integer, example: 1 } + designation_id: { type: integer, example: 1 } + plant_id: { type: integer, example: 1 } + reporting_to: { type: integer, example: 1 } + status: { type: string, enum: [active, inactive, locked], example: active } + is_active: { type: boolean, example: true } + RolesCreateBody: + type: object + required: [name] + properties: + name: { type: string, example: 'Purchase Manager' } + description: { type: string, example: 'Manages purchase orders' } + is_active: { type: boolean, example: true } + RolesUpdateBody: + type: object + minProperties: 1 + properties: + name: { type: string, example: 'Purchase Manager' } + description: { type: string, example: 'Manages purchase orders' } + is_active: { type: boolean, example: true } + RolesPermissionsBody: + type: object + properties: + permission_ids: + type: array + items: { type: integer } + example: [1, 2, 3, 4, 5, 6] + RolesPermissionMatrixBody: + type: object + required: [matrix] + properties: + matrix: + type: array + items: + type: object + required: [module_id, actions] + properties: + module_id: { type: integer, example: 1 } + actions: + type: object + required: [view, create, edit, delete, approve, export] + properties: + view: { type: boolean, example: true } + create: { type: boolean, example: true } + edit: { type: boolean, example: true } + delete: { type: boolean, example: false } + approve: { type: boolean, example: false } + export: { type: boolean, example: true } + +paths: + /auth/login: + post: + tags: [Auth] + summary: Login user and issue JWT/refresh tokens + security: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/AuthLoginBody" } + responses: + "200": + description: Login successful + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "401": { description: Invalid credentials } + /auth/refresh: + post: + tags: [Auth] + summary: Refresh access token + security: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/AuthRefreshBody" } + responses: + "200": + description: Success + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "401": { description: Invalid or expired token } + /auth/logout: + post: + tags: [Auth] + summary: Logout refresh token + security: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/AuthRefreshBody" } + responses: + "200": + description: Success + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "401": { description: Invalid or expired token } + /masters/uom: + get: + tags: [UOM] + summary: List UOM + parameters: + - in: query + name: page + schema: { type: integer, default: 1 } + - in: query + name: limit + schema: { type: integer, default: 20, maximum: 100 } + - in: query + name: search + schema: { type: string } + - in: query + name: is_active + schema: { type: boolean } + responses: + "200": + description: List fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + post: + tags: [UOM] + summary: Create UOM + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/UomCreateBody" } + responses: + "201": + description: Created + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "409": { description: Duplicate key } + /masters/uom/{id}: + parameters: + - in: path + name: id + required: true + schema: { type: string } + get: + tags: [UOM] + summary: Get UOM by id + responses: + "200": + description: Fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + put: + tags: [UOM] + summary: Update UOM + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/UomUpdateBody" } + responses: + "200": + description: Updated + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + delete: + tags: [UOM] + summary: Delete UOM + responses: + "200": + description: Deleted + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + /masters/item-categories: + get: + tags: [Item Categories] + summary: List Item Categories + parameters: + - in: query + name: page + schema: { type: integer, default: 1 } + - in: query + name: limit + schema: { type: integer, default: 20, maximum: 100 } + - in: query + name: search + schema: { type: string } + - in: query + name: is_active + schema: { type: boolean } + responses: + "200": + description: List fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + post: + tags: [Item Categories] + summary: Create Item Categorie + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/ItemCategoriesCreateBody" } + responses: + "201": + description: Created + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "409": { description: Duplicate key } + /masters/item-categories/{id}: + parameters: + - in: path + name: id + required: true + schema: { type: string } + get: + tags: [Item Categories] + summary: Get Item Categorie by id + responses: + "200": + description: Fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + put: + tags: [Item Categories] + summary: Update Item Categorie + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/ItemCategoriesUpdateBody" } + responses: + "200": + description: Updated + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + delete: + tags: [Item Categories] + summary: Delete Item Categorie + responses: + "200": + description: Deleted + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + /masters/item-subcategories: + get: + tags: [Item Subcategories] + summary: List Item Subcategories + parameters: + - in: query + name: page + schema: { type: integer, default: 1 } + - in: query + name: limit + schema: { type: integer, default: 20, maximum: 100 } + - in: query + name: search + schema: { type: string } + - in: query + name: is_active + schema: { type: boolean } + responses: + "200": + description: List fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + post: + tags: [Item Subcategories] + summary: Create Item Subcategorie + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/ItemSubcategoriesCreateBody" } + responses: + "201": + description: Created + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "409": { description: Duplicate key } + /masters/item-subcategories/{id}: + parameters: + - in: path + name: id + required: true + schema: { type: string } + get: + tags: [Item Subcategories] + summary: Get Item Subcategorie by id + responses: + "200": + description: Fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + put: + tags: [Item Subcategories] + summary: Update Item Subcategorie + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/ItemSubcategoriesUpdateBody" } + responses: + "200": + description: Updated + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + delete: + tags: [Item Subcategories] + summary: Delete Item Subcategorie + responses: + "200": + description: Deleted + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + /masters/items: + get: + tags: [Items] + summary: List Items + parameters: + - in: query + name: page + schema: { type: integer, default: 1 } + - in: query + name: limit + schema: { type: integer, default: 20, maximum: 100 } + - in: query + name: search + schema: { type: string } + - in: query + name: is_active + schema: { type: boolean } + responses: + "200": + description: List fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + post: + tags: [Items] + summary: Create Item + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/ItemsCreateBody" } + responses: + "201": + description: Created + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "409": { description: Duplicate key } + /masters/items/{id}: + parameters: + - in: path + name: id + required: true + schema: { type: string } + get: + tags: [Items] + summary: Get Item by id + responses: + "200": + description: Fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + put: + tags: [Items] + summary: Update Item + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/ItemsUpdateBody" } + responses: + "200": + description: Updated + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + delete: + tags: [Items] + summary: Delete Item + responses: + "200": + description: Deleted + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + /masters/brands: + get: + tags: [Brands] + summary: List Brands + parameters: + - in: query + name: page + schema: { type: integer, default: 1 } + - in: query + name: limit + schema: { type: integer, default: 20, maximum: 100 } + - in: query + name: search + schema: { type: string } + - in: query + name: is_active + schema: { type: boolean } + responses: + "200": + description: List fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + post: + tags: [Brands] + summary: Create Brand + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/BrandsCreateBody" } + responses: + "201": + description: Created + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "409": { description: Duplicate key } + /masters/brands/{id}: + parameters: + - in: path + name: id + required: true + schema: { type: string } + get: + tags: [Brands] + summary: Get Brand by id + responses: + "200": + description: Fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + put: + tags: [Brands] + summary: Update Brand + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/BrandsUpdateBody" } + responses: + "200": + description: Updated + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + delete: + tags: [Brands] + summary: Delete Brand + responses: + "200": + description: Deleted + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + /masters/gst-rates: + get: + tags: [GST Rates] + summary: List GST Rates + parameters: + - in: query + name: page + schema: { type: integer, default: 1 } + - in: query + name: limit + schema: { type: integer, default: 20, maximum: 100 } + - in: query + name: search + schema: { type: string } + - in: query + name: is_active + schema: { type: boolean } + responses: + "200": + description: List fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + post: + tags: [GST Rates] + summary: Create GST Rate + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/GstRatesCreateBody" } + responses: + "201": + description: Created + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "409": { description: Duplicate key } + /masters/gst-rates/{id}: + parameters: + - in: path + name: id + required: true + schema: { type: string } + get: + tags: [GST Rates] + summary: Get GST Rate by id + responses: + "200": + description: Fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + put: + tags: [GST Rates] + summary: Update GST Rate + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/GstRatesUpdateBody" } + responses: + "200": + description: Updated + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + delete: + tags: [GST Rates] + summary: Delete GST Rate + responses: + "200": + description: Deleted + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + /masters/payment-terms: + get: + tags: [Payment Terms] + summary: List Payment Terms + parameters: + - in: query + name: page + schema: { type: integer, default: 1 } + - in: query + name: limit + schema: { type: integer, default: 20, maximum: 100 } + - in: query + name: search + schema: { type: string } + - in: query + name: is_active + schema: { type: boolean } + responses: + "200": + description: List fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + post: + tags: [Payment Terms] + summary: Create Payment Term + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/PaymentTermsCreateBody" } + responses: + "201": + description: Created + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "409": { description: Duplicate key } + /masters/payment-terms/{id}: + parameters: + - in: path + name: id + required: true + schema: { type: string } + get: + tags: [Payment Terms] + summary: Get Payment Term by id + responses: + "200": + description: Fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + put: + tags: [Payment Terms] + summary: Update Payment Term + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/PaymentTermsUpdateBody" } + responses: + "200": + description: Updated + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + delete: + tags: [Payment Terms] + summary: Delete Payment Term + responses: + "200": + description: Deleted + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + /masters/delivery-terms: + get: + tags: [Delivery Terms] + summary: List Delivery Terms + parameters: + - in: query + name: page + schema: { type: integer, default: 1 } + - in: query + name: limit + schema: { type: integer, default: 20, maximum: 100 } + - in: query + name: search + schema: { type: string } + - in: query + name: is_active + schema: { type: boolean } + responses: + "200": + description: List fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + post: + tags: [Delivery Terms] + summary: Create Delivery Term + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/DeliveryTermsCreateBody" } + responses: + "201": + description: Created + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "409": { description: Duplicate key } + /masters/delivery-terms/{id}: + parameters: + - in: path + name: id + required: true + schema: { type: string } + get: + tags: [Delivery Terms] + summary: Get Delivery Term by id + responses: + "200": + description: Fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + put: + tags: [Delivery Terms] + summary: Update Delivery Term + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/DeliveryTermsUpdateBody" } + responses: + "200": + description: Updated + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + delete: + tags: [Delivery Terms] + summary: Delete Delivery Term + responses: + "200": + description: Deleted + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + /masters/asset-categories: + get: + tags: [Asset Categories] + summary: List Asset Categories + parameters: + - in: query + name: page + schema: { type: integer, default: 1 } + - in: query + name: limit + schema: { type: integer, default: 20, maximum: 100 } + - in: query + name: search + schema: { type: string } + - in: query + name: is_active + schema: { type: boolean } + responses: + "200": + description: List fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + post: + tags: [Asset Categories] + summary: Create Asset Categorie + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/AssetCategoriesCreateBody" } + responses: + "201": + description: Created + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "409": { description: Duplicate key } + /masters/asset-categories/{id}: + parameters: + - in: path + name: id + required: true + schema: { type: string } + get: + tags: [Asset Categories] + summary: Get Asset Categorie by id + responses: + "200": + description: Fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + put: + tags: [Asset Categories] + summary: Update Asset Categorie + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/AssetCategoriesUpdateBody" } + responses: + "200": + description: Updated + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + delete: + tags: [Asset Categories] + summary: Delete Asset Categorie + responses: + "200": + description: Deleted + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + /masters/departments: + get: + tags: [Departments] + summary: List Departments + parameters: + - in: query + name: page + schema: { type: integer, default: 1 } + - in: query + name: limit + schema: { type: integer, default: 20, maximum: 100 } + - in: query + name: search + schema: { type: string } + - in: query + name: is_active + schema: { type: boolean } + responses: + "200": + description: List fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + post: + tags: [Departments] + summary: Create Department + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/DepartmentsCreateBody" } + responses: + "201": + description: Created + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "409": { description: Duplicate key } + /masters/departments/{id}: + parameters: + - in: path + name: id + required: true + schema: { type: string } + get: + tags: [Departments] + summary: Get Department by id + responses: + "200": + description: Fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + put: + tags: [Departments] + summary: Update Department + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/DepartmentsUpdateBody" } + responses: + "200": + description: Updated + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + delete: + tags: [Departments] + summary: Delete Department + responses: + "200": + description: Deleted + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + /masters/designations: + get: + tags: [Designations] + summary: List Designations + parameters: + - in: query + name: page + schema: { type: integer, default: 1 } + - in: query + name: limit + schema: { type: integer, default: 20, maximum: 100 } + - in: query + name: search + schema: { type: string } + - in: query + name: is_active + schema: { type: boolean } + responses: + "200": + description: List fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + post: + tags: [Designations] + summary: Create Designation + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/DesignationsCreateBody" } + responses: + "201": + description: Created + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "409": { description: Duplicate key } + /masters/designations/{id}: + parameters: + - in: path + name: id + required: true + schema: { type: string } + get: + tags: [Designations] + summary: Get Designation by id + responses: + "200": + description: Fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + put: + tags: [Designations] + summary: Update Designation + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/DesignationsUpdateBody" } + responses: + "200": + description: Updated + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + delete: + tags: [Designations] + summary: Delete Designation + responses: + "200": + description: Deleted + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + /masters/warehouses: + get: + tags: [Warehouses] + summary: List Warehouses + parameters: + - in: query + name: page + schema: { type: integer, default: 1 } + - in: query + name: limit + schema: { type: integer, default: 20, maximum: 100 } + - in: query + name: search + schema: { type: string } + - in: query + name: is_active + schema: { type: boolean } + responses: + "200": + description: List fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + post: + tags: [Warehouses] + summary: Create Warehouse + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/WarehousesCreateBody" } + responses: + "201": + description: Created + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "409": { description: Duplicate key } + /masters/warehouses/{id}: + parameters: + - in: path + name: id + required: true + schema: { type: string } + get: + tags: [Warehouses] + summary: Get Warehouse by id + responses: + "200": + description: Fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + put: + tags: [Warehouses] + summary: Update Warehouse + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/WarehousesUpdateBody" } + responses: + "200": + description: Updated + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + delete: + tags: [Warehouses] + summary: Delete Warehouse + responses: + "200": + description: Deleted + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + /masters/plants: + get: + tags: [Plants] + summary: List Plants + parameters: + - in: query + name: page + schema: { type: integer, default: 1 } + - in: query + name: limit + schema: { type: integer, default: 20, maximum: 100 } + - in: query + name: search + schema: { type: string } + - in: query + name: is_active + schema: { type: boolean } + responses: + "200": + description: List fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + post: + tags: [Plants] + summary: Create Plant + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/PlantsCreateBody" } + responses: + "201": + description: Created + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "409": { description: Duplicate key } + /masters/plants/{id}: + parameters: + - in: path + name: id + required: true + schema: { type: string } + get: + tags: [Plants] + summary: Get Plant by id + responses: + "200": + description: Fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + put: + tags: [Plants] + summary: Update Plant + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/PlantsUpdateBody" } + responses: + "200": + description: Updated + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + delete: + tags: [Plants] + summary: Delete Plant + responses: + "200": + description: Deleted + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + /masters/document-series: + get: + tags: [Document Series] + summary: List Document Series + parameters: + - in: query + name: page + schema: { type: integer, default: 1 } + - in: query + name: limit + schema: { type: integer, default: 20, maximum: 100 } + - in: query + name: search + schema: { type: string } + - in: query + name: is_active + schema: { type: boolean } + responses: + "200": + description: List fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + post: + tags: [Document Series] + summary: Create Document Serie + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/DocumentSeriesCreateBody" } + responses: + "201": + description: Created + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "409": { description: Duplicate key } + /masters/document-series/{id}: + parameters: + - in: path + name: id + required: true + schema: { type: string } + get: + tags: [Document Series] + summary: Get Document Serie by id + responses: + "200": + description: Fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + put: + tags: [Document Series] + summary: Update Document Serie + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/DocumentSeriesUpdateBody" } + responses: + "200": + description: Updated + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + delete: + tags: [Document Series] + summary: Delete Document Serie + responses: + "200": + description: Deleted + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + + /users/summary: + get: + tags: [Users] + summary: Dashboard summary (total/active/inactive/locked users, roles count) + responses: + "200": + description: Summary fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + /users/filters: + get: + tags: [Users] + summary: Filter dropdown options (roles, departments, statuses) + responses: + "200": + description: Filters fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + /users/export: + get: + tags: [Users] + summary: Export users CSV (same filters as list) + parameters: + - in: query + name: search + schema: { type: string } + - in: query + name: status + schema: { type: string, enum: [active, inactive, locked] } + - in: query + name: role_id + schema: { type: integer } + - in: query + name: department_id + schema: { type: integer } + responses: + "200": + description: CSV file + content: + text/csv: + schema: { type: string } + + /users: + get: + tags: [Users] + summary: List users + description: Returns table rows with initials, role, department, plant, last_login_at, status + parameters: + - in: query + name: page + schema: { type: integer, default: 1 } + - in: query + name: limit + schema: { type: integer, default: 20, maximum: 100 } + - in: query + name: search + schema: { type: string } + - in: query + name: status + schema: { type: string, enum: [active, inactive, locked] } + - in: query + name: is_active + schema: { type: boolean } + - in: query + name: role_id + schema: { type: integer } + - in: query + name: department_id + schema: { type: integer } + responses: + "200": + description: List fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + post: + tags: [Users] + summary: Create user + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/UsersCreateBody" } + responses: + "201": + description: Created + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "409": { description: Duplicate email or employee code } + /users/{id}: + parameters: + - in: path + name: id + required: true + schema: { type: string } + get: + tags: [Users] + summary: Get user by id + responses: + "200": + description: Fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + put: + tags: [Users] + summary: Update user + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/UsersUpdateBody" } + responses: + "200": + description: Updated + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + delete: + tags: [Users] + summary: Delete user (soft delete) + responses: + "200": + description: Deleted + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + + /roles/permissions: + get: + tags: [Roles] + summary: List permission catalog (modules and actions) + responses: + "200": + description: Catalog fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + /roles/cards: + get: + tags: [Roles] + summary: Role cards for Roles tab (name, description, user_count, permission_count) + responses: + "200": + description: Cards fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + /roles: + get: + tags: [Roles] + summary: List roles + parameters: + - in: query + name: page + schema: { type: integer, default: 1 } + - in: query + name: limit + schema: { type: integer, default: 20, maximum: 100 } + - in: query + name: search + schema: { type: string } + - in: query + name: is_active + schema: { type: boolean } + responses: + "200": + description: List fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + post: + tags: [Roles] + summary: Create role + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/RolesCreateBody" } + responses: + "201": + description: Created + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "409": { description: Duplicate role name } + /roles/{id}: + parameters: + - in: path + name: id + required: true + schema: { type: string } + get: + tags: [Roles] + summary: Get role by id with permissions + responses: + "200": + description: Fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + put: + tags: [Roles] + summary: Update role + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/RolesUpdateBody" } + responses: + "200": + description: Updated + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + delete: + tags: [Roles] + summary: Delete role (soft delete) + responses: + "200": + description: Deleted + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "400": { description: Role assigned to users } + "404": { description: Not found } + /roles/{id}/permissions: + parameters: + - in: path + name: id + required: true + schema: { type: string } + put: + tags: [Roles] + summary: Assign permissions to role (replaces existing) + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/RolesPermissionsBody" } + responses: + "200": + description: Permissions updated + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + "422": { description: Invalid permission_ids } + /roles/{id}/permission-matrix: + parameters: + - in: path + name: id + required: true + schema: { type: string } + get: + tags: [Roles] + summary: Permission matrix for role (modules x actions checkboxes) + responses: + "200": + description: Matrix fetched + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + put: + tags: [Roles] + summary: Save permission matrix for role + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/RolesPermissionMatrixBody" } + responses: + "200": + description: Matrix saved + content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } } + "404": { description: Not found } + "422": { description: Invalid matrix } diff --git a/src/middlewares/auth.middleware.js b/src/middlewares/auth.middleware.js new file mode 100644 index 0000000..24dfe77 --- /dev/null +++ b/src/middlewares/auth.middleware.js @@ -0,0 +1,39 @@ +const jwt = require('jsonwebtoken'); +const ApiError = require('../utils/ApiError'); +const env = require('../config/env'); +const prisma = require('../config/prisma'); + +module.exports = async (req, _res, next) => { + try { + const header = req.headers.authorization; + if (!header || !header.startsWith('Bearer ')) { + throw new ApiError(401, 'Authentication token missing'); + } + + const token = header.split(' ')[1]; + const payload = jwt.verify(token, env.JWT_ACCESS_SECRET); + + const user = await prisma.users.findFirst({ + where: { id: BigInt(payload.sub), deleted_at: null }, + include: { + roles_users_role_idToroles: { + include: { + role_permissions: { include: { permissions: { include: { modules: true } } } }, + }, + }, + }, + }); + + if (!user || !user.is_active || user.status !== 'active') { + throw new ApiError(401, 'Invalid or inactive user'); + } + + user.role = user.roles_users_role_idToroles; + req.user = user; + return next(); + } catch (err) { + if (err.name === 'TokenExpiredError') return next(new ApiError(401, 'Access token expired')); + if (err.name === 'JsonWebTokenError') return next(new ApiError(401, 'Invalid access token')); + return next(err instanceof ApiError ? err : new ApiError(401, 'Unauthorized')); + } +}; diff --git a/src/middlewares/error.middleware.js b/src/middlewares/error.middleware.js new file mode 100644 index 0000000..7b617ae --- /dev/null +++ b/src/middlewares/error.middleware.js @@ -0,0 +1,31 @@ +const ApiError = require('../utils/ApiError'); +const logger = require('../config/logger'); +const env = require('../config/env'); + +module.exports = (err, req, res, _next) => { + let statusCode = err.statusCode || 500; + let message = err.message || 'Internal server error'; + let errors = err.errors || []; + + if (!(err instanceof ApiError)) { + statusCode = 500; + message = env.NODE_ENV === 'production' ? 'Internal server error' : err.message; + errors = []; + } + + logger.error(message, { + requestId: req.id, + statusCode, + path: req.originalUrl, + method: req.method, + user: req.user ? req.user.id.toString() : undefined, + stack: err.stack, + }); + + return res.status(statusCode).json({ + success: false, + message, + errors, + ...(env.NODE_ENV === 'development' && { stack: err.stack }), + }); +}; diff --git a/src/middlewares/rateLimiter.middleware.js b/src/middlewares/rateLimiter.middleware.js new file mode 100644 index 0000000..9eb656b --- /dev/null +++ b/src/middlewares/rateLimiter.middleware.js @@ -0,0 +1,21 @@ +const rateLimit = require('express-rate-limit'); +const env = require('../config/env'); + +const generalLimiter = rateLimit({ + windowMs: env.RATE_LIMIT_WINDOW_MS, + max: env.RATE_LIMIT_MAX, + standardHeaders: true, + legacyHeaders: false, + message: { success: false, message: 'Too many requests, please try again later' }, +}); + +const authLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: env.AUTH_RATE_LIMIT_MAX, + standardHeaders: true, + legacyHeaders: false, + skipSuccessfulRequests: true, + message: { success: false, message: 'Too many login attempts, please try again later' }, +}); + +module.exports = { generalLimiter, authLimiter }; diff --git a/src/middlewares/rbac.middleware.js b/src/middlewares/rbac.middleware.js new file mode 100644 index 0000000..c53ad41 --- /dev/null +++ b/src/middlewares/rbac.middleware.js @@ -0,0 +1,13 @@ +const ApiError = require('../utils/ApiError'); + +const authorize = (moduleCode, action) => (req, _res, next) => { + const permissions = req.user?.role?.role_permissions || []; + const allowed = permissions.some( + (rp) => rp.permissions.modules.code === moduleCode && rp.permissions.action === action + ); + + if (!allowed) return next(new ApiError(403, `Forbidden: requires ${moduleCode}:${action}`)); + return next(); +}; + +module.exports = authorize; diff --git a/src/middlewares/requestId.middleware.js b/src/middlewares/requestId.middleware.js new file mode 100644 index 0000000..31529bf --- /dev/null +++ b/src/middlewares/requestId.middleware.js @@ -0,0 +1,7 @@ +const { v4: uuidv4 } = require('uuid'); + +module.exports = (req, res, next) => { + req.id = req.headers['x-request-id'] || uuidv4(); + res.setHeader('X-Request-Id', req.id); + next(); +}; diff --git a/src/middlewares/upload.middleware.js b/src/middlewares/upload.middleware.js new file mode 100644 index 0000000..88a2047 --- /dev/null +++ b/src/middlewares/upload.middleware.js @@ -0,0 +1,28 @@ +const multer = require('multer'); +const path = require('path'); +const crypto = require('crypto'); +const ApiError = require('../utils/ApiError'); +const env = require('../config/env'); + +const ALLOWED_MIME = ['application/pdf', 'image/jpeg', 'image/png', 'image/webp']; + +const storage = multer.diskStorage({ + destination: (_req, _file, cb) => cb(null, env.UPLOAD_DIR), + filename: (_req, file, cb) => { + const unique = crypto.randomBytes(16).toString('hex'); + cb(null, `${unique}${path.extname(file.originalname).toLowerCase()}`); + }, +}); + +const fileFilter = (_req, file, cb) => { + if (!ALLOWED_MIME.includes(file.mimetype)) { + return cb(new ApiError(400, `Unsupported file type: ${file.mimetype}`), false); + } + return cb(null, true); +}; + +module.exports = multer({ + storage, + fileFilter, + limits: { fileSize: env.MAX_FILE_SIZE_MB * 1024 * 1024 }, +}); diff --git a/src/middlewares/validate.middleware.js b/src/middlewares/validate.middleware.js new file mode 100644 index 0000000..6930850 --- /dev/null +++ b/src/middlewares/validate.middleware.js @@ -0,0 +1,20 @@ +const ApiError = require('../utils/ApiError'); + +const validate = + (schema, source = 'body') => + (req, _res, next) => { + const { error, value } = schema.validate(req[source], { + abortEarly: false, + stripUnknown: true, + }); + + if (error) { + const errors = error.details.map((d) => ({ field: d.path.join('.'), message: d.message })); + return next(new ApiError(422, 'Validation failed', errors)); + } + + req[source] = value; + return next(); + }; + +module.exports = validate; diff --git a/src/modules/auth/auth.controller.js b/src/modules/auth/auth.controller.js new file mode 100644 index 0000000..f3e81ee --- /dev/null +++ b/src/modules/auth/auth.controller.js @@ -0,0 +1,20 @@ +const asyncHandler = require('../../utils/asyncHandler'); +const ApiResponse = require('../../utils/ApiResponse'); +const authService = require('./auth.service'); + +const login = asyncHandler(async (req, res) => { + const tokens = await authService.login(req.body.email, req.body.password); + res.json(new ApiResponse(200, tokens, 'Login successful')); +}); + +const refresh = asyncHandler(async (req, res) => { + const tokens = await authService.refresh(req.body.refresh_token); + res.json(new ApiResponse(200, tokens, 'Token refreshed')); +}); + +const logout = asyncHandler(async (req, res) => { + await authService.logout(req.body.refresh_token); + res.json(new ApiResponse(200, null, 'Logged out')); +}); + +module.exports = { login, refresh, logout }; diff --git a/src/modules/auth/auth.routes.js b/src/modules/auth/auth.routes.js new file mode 100644 index 0000000..9e89421 --- /dev/null +++ b/src/modules/auth/auth.routes.js @@ -0,0 +1,13 @@ +const express = require('express'); +const validate = require('../../middlewares/validate.middleware'); +const { authLimiter } = require('../../middlewares/rateLimiter.middleware'); +const { loginSchema, refreshSchema } = require('./auth.validation'); +const controller = require('./auth.controller'); + +const router = express.Router(); + +router.post('/login', authLimiter, validate(loginSchema), controller.login); +router.post('/refresh', validate(refreshSchema), controller.refresh); +router.post('/logout', validate(refreshSchema), controller.logout); + +module.exports = router; diff --git a/src/modules/auth/auth.service.js b/src/modules/auth/auth.service.js new file mode 100644 index 0000000..3334f85 --- /dev/null +++ b/src/modules/auth/auth.service.js @@ -0,0 +1,94 @@ +const bcrypt = require('bcrypt'); +const jwt = require('jsonwebtoken'); +const crypto = require('crypto'); +const prisma = require('../../config/prisma'); +const ApiError = require('../../utils/ApiError'); +const env = require('../../config/env'); + +const hashToken = (token) => crypto.createHash('sha256').update(token).digest('hex'); + +const issueTokens = async (user) => { + const accessToken = jwt.sign( + { sub: user.id.toString(), role_id: user.role_id?.toString() }, + env.JWT_ACCESS_SECRET, + { expiresIn: env.JWT_ACCESS_EXPIRY } + ); + + const refreshToken = crypto.randomBytes(40).toString('hex'); + await prisma.refresh_tokens.create({ + data: { + user_id: user.id, + token_hash: hashToken(refreshToken), + expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), + }, + }); + + return { accessToken, refreshToken }; +}; + +const login = async (email, password) => { + const user = await prisma.users.findFirst({ where: { email, deleted_at: null } }); + if (!user) throw new ApiError(401, 'Invalid email or password'); + + if (user.locked_until && user.locked_until > new Date()) { + throw new ApiError(423, 'Account locked due to too many failed attempts. Try again later.'); + } + + const match = await bcrypt.compare(password, user.password_hash); + if (!match) { + const attempts = user.failed_login_attempts + 1; + const lockout = attempts >= env.MAX_LOGIN_ATTEMPTS; + await prisma.users.update({ + where: { id: user.id }, + data: { + failed_login_attempts: lockout ? 0 : attempts, + locked_until: lockout + ? new Date(Date.now() + env.LOCKOUT_DURATION_MINUTES * 60 * 1000) + : null, + }, + }); + throw new ApiError(401, 'Invalid email or password'); + } + + if (user.status !== 'active' || !user.is_active) { + throw new ApiError(403, 'Account is inactive. Contact administrator.'); + } + + await prisma.users.update({ + where: { id: user.id }, + data: { failed_login_attempts: 0, locked_until: null, last_login_at: new Date() }, + }); + + return issueTokens(user); +}; + +const refresh = async (refreshToken) => { + const tokenHash = hashToken(refreshToken); + const record = await prisma.refresh_tokens.findFirst({ + where: { token_hash: tokenHash, revoked_at: null }, + }); + + if (!record || record.expires_at < new Date()) { + throw new ApiError(401, 'Invalid or expired refresh token'); + } + + await prisma.refresh_tokens.update({ + where: { id: record.id }, + data: { revoked_at: new Date() }, + }); + + const user = await prisma.users.findUnique({ where: { id: record.user_id } }); + if (!user || !user.is_active) throw new ApiError(401, 'User not found or inactive'); + + return issueTokens(user); +}; + +const logout = async (refreshToken) => { + const tokenHash = hashToken(refreshToken); + await prisma.refresh_tokens.updateMany({ + where: { token_hash: tokenHash, revoked_at: null }, + data: { revoked_at: new Date() }, + }); +}; + +module.exports = { login, refresh, logout }; diff --git a/src/modules/auth/auth.validation.js b/src/modules/auth/auth.validation.js new file mode 100644 index 0000000..b48a2a6 --- /dev/null +++ b/src/modules/auth/auth.validation.js @@ -0,0 +1,12 @@ +const Joi = require('joi'); + +const loginSchema = Joi.object({ + email: Joi.string().email().required(), + password: Joi.string().min(8).required(), +}); + +const refreshSchema = Joi.object({ + refresh_token: Joi.string().required(), +}); + +module.exports = { loginSchema, refreshSchema }; diff --git a/src/modules/masters/_shared/master.factory.js b/src/modules/masters/_shared/master.factory.js new file mode 100644 index 0000000..cb9d4fa --- /dev/null +++ b/src/modules/masters/_shared/master.factory.js @@ -0,0 +1,148 @@ +const prisma = require('../../../config/prisma'); +const ApiError = require('../../../utils/ApiError'); +const auditLog = require('../../../utils/auditLog'); +const { getPagination } = require('../../../utils/pagination'); + +const normalizePayload = (payload, fields) => { + const out = { ...payload }; + for (const f of fields) { + if (Object.prototype.hasOwnProperty.call(out, f.name)) { + if (f.type === 'string' && out[f.name] !== null && out[f.name] !== undefined) { + out[f.name] = String(out[f.name]).trim(); + if (f.uppercase) out[f.name] = out[f.name].toUpperCase(); + } + if (f.type === 'int' && out[f.name] !== null && out[f.name] !== undefined) { + out[f.name] = Number(out[f.name]); + } + if (f.type === 'decimal' && out[f.name] !== null && out[f.name] !== undefined) { + out[f.name] = Number(out[f.name]); + } + } + } + return out; +}; + +const buildMasterService = ({ + modelName, + tableName, + fields, + uniqueField = null, + softDelete = true, +}) => { + const model = prisma[modelName]; + + const createOne = async (payload, userId, requestId) => { + const data = normalizePayload(payload, fields); + + if (uniqueField && data[uniqueField] !== undefined) { + const existing = await model.findFirst({ + where: { + [uniqueField]: data[uniqueField], + ...(softDelete ? { deleted_at: null } : {}), + }, + }); + if (existing) throw new ApiError(409, `${tableName} ${uniqueField} already exists`); + } + + if (fields.some((f) => f.name === 'created_by')) + data.created_by = userId ? BigInt(userId) : null; + if (fields.some((f) => f.name === 'updated_by')) + data.updated_by = userId ? BigInt(userId) : null; + + const created = await model.create({ data }); + await auditLog({ + tableName, + recordId: created.id, + action: 'CREATE', + oldValue: null, + newValue: created, + userId, + requestId, + }); + return created; + }; + + const list = async (query) => { + const { page, limit, skip } = getPagination(query); + const searchFields = fields.filter((f) => f.searchable).map((f) => f.name); + + const where = { + ...(softDelete ? { deleted_at: null } : {}), + ...(query.is_active !== undefined ? { is_active: query.is_active } : {}), + ...(query.search && searchFields.length + ? { + OR: searchFields.map((name) => ({ + [name]: { contains: query.search, mode: 'insensitive' }, + })), + } + : {}), + }; + + const [data, total] = await Promise.all([ + model.findMany({ where, orderBy: { created_at: 'desc' }, skip, take: limit }), + model.count({ where }), + ]); + + return { data, meta: { page, limit, total } }; + }; + + const getOne = async (id) => { + const one = await model.findFirst({ + where: { id: BigInt(id), ...(softDelete ? { deleted_at: null } : {}) }, + }); + if (!one) throw new ApiError(404, `${tableName} not found`); + return one; + }; + + const updateOne = async (id, payload, userId, requestId) => { + const existing = await getOne(id); + const data = normalizePayload(payload, fields); + if (fields.some((f) => f.name === 'updated_by')) + data.updated_by = userId ? BigInt(userId) : null; + + const updated = await model.update({ where: { id: BigInt(id) }, data }); + + await auditLog({ + tableName, + recordId: id, + action: 'UPDATE', + oldValue: existing, + newValue: updated, + userId, + requestId, + }); + + return updated; + }; + + const removeOne = async (id, userId, requestId) => { + const existing = await getOne(id); + if (softDelete) { + await model.update({ + where: { id: BigInt(id) }, + data: { + deleted_at: new Date(), + ...(fields.some((f) => f.name === 'updated_by') + ? { updated_by: userId ? BigInt(userId) : null } + : {}), + }, + }); + } else { + await model.delete({ where: { id: BigInt(id) } }); + } + + await auditLog({ + tableName, + recordId: id, + action: 'DELETE', + oldValue: existing, + newValue: softDelete ? { deleted_at: new Date() } : null, + userId, + requestId, + }); + }; + + return { createOne, list, getOne, updateOne, removeOne }; +}; + +module.exports = { buildMasterService }; diff --git a/src/modules/masters/asset-categories/asset-categories.controller.js b/src/modules/masters/asset-categories/asset-categories.controller.js new file mode 100644 index 0000000..f307ab4 --- /dev/null +++ b/src/modules/masters/asset-categories/asset-categories.controller.js @@ -0,0 +1,30 @@ +const asyncHandler = require('../../../utils/asyncHandler'); +const ApiResponse = require('../../../utils/ApiResponse'); +const service = require('./asset-categories.service'); + +const create = asyncHandler(async (req, res) => { + const data = await service.createAssetCategories(req.body, req.user?.id, req.id); + res.status(201).json(new ApiResponse(201, data, 'asset_categories created successfully')); +}); + +const list = asyncHandler(async (req, res) => { + const result = await service.listAssetCategories(req.query); + res.json(new ApiResponse(200, result.data, 'asset_categories list fetched', result.meta)); +}); + +const getOne = asyncHandler(async (req, res) => { + const data = await service.getAssetCategoriesById(req.params.id); + res.json(new ApiResponse(200, data, 'asset_categories fetched')); +}); + +const update = asyncHandler(async (req, res) => { + const data = await service.updateAssetCategories(req.params.id, req.body, req.user?.id, req.id); + res.json(new ApiResponse(200, data, 'asset_categories updated successfully')); +}); + +const remove = asyncHandler(async (req, res) => { + await service.deleteAssetCategories(req.params.id, req.user?.id, req.id); + res.json(new ApiResponse(200, null, 'asset_categories deleted successfully')); +}); + +module.exports = { create, list, getOne, update, remove }; diff --git a/src/modules/masters/asset-categories/asset-categories.routes.js b/src/modules/masters/asset-categories/asset-categories.routes.js new file mode 100644 index 0000000..38e5684 --- /dev/null +++ b/src/modules/masters/asset-categories/asset-categories.routes.js @@ -0,0 +1,17 @@ +const express = require('express'); +const authenticate = require('../../../middlewares/auth.middleware'); +const authorize = require('../../../middlewares/rbac.middleware'); +const validate = require('../../../middlewares/validate.middleware'); +const controller = require('./asset-categories.controller'); +const { createSchema, updateSchema, listQuerySchema } = require('./asset-categories.validation'); + +const router = express.Router(); + +router.use(authenticate); +router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list); +router.get('/:id', authorize('MASTERS', 'view'), controller.getOne); +router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create); +router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update); +router.delete('/:id', authorize('MASTERS', 'delete'), controller.remove); + +module.exports = router; diff --git a/src/modules/masters/asset-categories/asset-categories.service.js b/src/modules/masters/asset-categories/asset-categories.service.js new file mode 100644 index 0000000..eafcd5f --- /dev/null +++ b/src/modules/masters/asset-categories/asset-categories.service.js @@ -0,0 +1,68 @@ +const { buildMasterService } = require('../_shared/master.factory'); + +const config = { + modelName: 'asset_categories', + tableName: 'asset_categories', + uniqueField: 'code', + softDelete: true, + fields: [ + { + name: 'code', + type: 'string', + uppercase: true, + searchable: true, + }, + { + name: 'name', + type: 'string', + uppercase: false, + searchable: true, + }, + { + name: 'code_prefix', + type: 'string', + uppercase: true, + searchable: false, + }, + { + name: 'default_useful_life_years', + type: 'int', + uppercase: false, + searchable: false, + }, + { + name: 'default_depreciation_method', + type: 'string', + uppercase: false, + searchable: false, + }, + { + name: 'is_active', + type: 'boolean', + uppercase: false, + searchable: false, + }, + { + name: 'created_by', + type: 'int', + uppercase: false, + searchable: false, + }, + { + name: 'updated_by', + type: 'int', + uppercase: false, + searchable: false, + }, + ], +}; + +const service = buildMasterService(config); + +module.exports = { + createAssetCategories: service.createOne, + listAssetCategories: service.list, + getAssetCategoriesById: service.getOne, + updateAssetCategories: service.updateOne, + deleteAssetCategories: service.removeOne, +}; diff --git a/src/modules/masters/asset-categories/asset-categories.validation.js b/src/modules/masters/asset-categories/asset-categories.validation.js new file mode 100644 index 0000000..0f59161 --- /dev/null +++ b/src/modules/masters/asset-categories/asset-categories.validation.js @@ -0,0 +1,21 @@ +const Joi = require('joi'); + +const createSchema = Joi.object({ + code: Joi.string().allow('').optional(), + name: Joi.string().allow('').optional(), + code_prefix: Joi.string().allow('').optional(), + default_useful_life_years: Joi.number().integer().optional(), + default_depreciation_method: Joi.string().allow('').optional(), + is_active: Joi.boolean().optional(), +}); + +const updateSchema = createSchema.min(1); + +const listQuerySchema = Joi.object({ + page: Joi.number().integer().min(1).default(1), + limit: Joi.number().integer().min(1).max(100).default(20), + search: Joi.string().allow('').optional(), + is_active: Joi.boolean().optional(), +}); + +module.exports = { createSchema, updateSchema, listQuerySchema }; diff --git a/src/modules/masters/brands/brands.controller.js b/src/modules/masters/brands/brands.controller.js new file mode 100644 index 0000000..a21f053 --- /dev/null +++ b/src/modules/masters/brands/brands.controller.js @@ -0,0 +1,30 @@ +const asyncHandler = require('../../../utils/asyncHandler'); +const ApiResponse = require('../../../utils/ApiResponse'); +const service = require('./brands.service'); + +const create = asyncHandler(async (req, res) => { + const data = await service.createBrands(req.body, req.user?.id, req.id); + res.status(201).json(new ApiResponse(201, data, 'brands created successfully')); +}); + +const list = asyncHandler(async (req, res) => { + const result = await service.listBrands(req.query); + res.json(new ApiResponse(200, result.data, 'brands list fetched', result.meta)); +}); + +const getOne = asyncHandler(async (req, res) => { + const data = await service.getBrandsById(req.params.id); + res.json(new ApiResponse(200, data, 'brands fetched')); +}); + +const update = asyncHandler(async (req, res) => { + const data = await service.updateBrands(req.params.id, req.body, req.user?.id, req.id); + res.json(new ApiResponse(200, data, 'brands updated successfully')); +}); + +const remove = asyncHandler(async (req, res) => { + await service.deleteBrands(req.params.id, req.user?.id, req.id); + res.json(new ApiResponse(200, null, 'brands deleted successfully')); +}); + +module.exports = { create, list, getOne, update, remove }; diff --git a/src/modules/masters/brands/brands.routes.js b/src/modules/masters/brands/brands.routes.js new file mode 100644 index 0000000..151d1ae --- /dev/null +++ b/src/modules/masters/brands/brands.routes.js @@ -0,0 +1,17 @@ +const express = require('express'); +const authenticate = require('../../../middlewares/auth.middleware'); +const authorize = require('../../../middlewares/rbac.middleware'); +const validate = require('../../../middlewares/validate.middleware'); +const controller = require('./brands.controller'); +const { createSchema, updateSchema, listQuerySchema } = require('./brands.validation'); + +const router = express.Router(); + +router.use(authenticate); +router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list); +router.get('/:id', authorize('MASTERS', 'view'), controller.getOne); +router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create); +router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update); +router.delete('/:id', authorize('MASTERS', 'delete'), controller.remove); + +module.exports = router; diff --git a/src/modules/masters/brands/brands.service.js b/src/modules/masters/brands/brands.service.js new file mode 100644 index 0000000..9443898 --- /dev/null +++ b/src/modules/masters/brands/brands.service.js @@ -0,0 +1,74 @@ +const { buildMasterService } = require('../_shared/master.factory'); + +const config = { + modelName: 'brands', + tableName: 'brands', + uniqueField: 'code', + softDelete: true, + fields: [ + { + name: 'code', + type: 'string', + uppercase: true, + searchable: true, + }, + { + name: 'name', + type: 'string', + uppercase: false, + searchable: true, + }, + { + name: 'brand_type', + type: 'string', + uppercase: false, + searchable: false, + }, + { + name: 'contact_person', + type: 'string', + uppercase: false, + searchable: false, + }, + { + name: 'phone', + type: 'string', + uppercase: false, + searchable: false, + }, + { + name: 'email', + type: 'string', + uppercase: false, + searchable: false, + }, + { + name: 'is_active', + type: 'boolean', + uppercase: false, + searchable: false, + }, + { + name: 'created_by', + type: 'int', + uppercase: false, + searchable: false, + }, + { + name: 'updated_by', + type: 'int', + uppercase: false, + searchable: false, + }, + ], +}; + +const service = buildMasterService(config); + +module.exports = { + createBrands: service.createOne, + listBrands: service.list, + getBrandsById: service.getOne, + updateBrands: service.updateOne, + deleteBrands: service.removeOne, +}; diff --git a/src/modules/masters/brands/brands.validation.js b/src/modules/masters/brands/brands.validation.js new file mode 100644 index 0000000..d49e00c --- /dev/null +++ b/src/modules/masters/brands/brands.validation.js @@ -0,0 +1,22 @@ +const Joi = require('joi'); + +const createSchema = Joi.object({ + code: Joi.string().allow('').optional(), + name: Joi.string().allow('').optional(), + brand_type: Joi.string().allow('').optional(), + contact_person: Joi.string().allow('').optional(), + phone: Joi.string().allow('').optional(), + email: Joi.string().allow('').optional(), + is_active: Joi.boolean().optional(), +}); + +const updateSchema = createSchema.min(1); + +const listQuerySchema = Joi.object({ + page: Joi.number().integer().min(1).default(1), + limit: Joi.number().integer().min(1).max(100).default(20), + search: Joi.string().allow('').optional(), + is_active: Joi.boolean().optional(), +}); + +module.exports = { createSchema, updateSchema, listQuerySchema }; diff --git a/src/modules/masters/delivery-terms/delivery-terms.controller.js b/src/modules/masters/delivery-terms/delivery-terms.controller.js new file mode 100644 index 0000000..4522c9e --- /dev/null +++ b/src/modules/masters/delivery-terms/delivery-terms.controller.js @@ -0,0 +1,30 @@ +const asyncHandler = require('../../../utils/asyncHandler'); +const ApiResponse = require('../../../utils/ApiResponse'); +const service = require('./delivery-terms.service'); + +const create = asyncHandler(async (req, res) => { + const data = await service.createDeliveryTerms(req.body, req.user?.id, req.id); + res.status(201).json(new ApiResponse(201, data, 'delivery_terms created successfully')); +}); + +const list = asyncHandler(async (req, res) => { + const result = await service.listDeliveryTerms(req.query); + res.json(new ApiResponse(200, result.data, 'delivery_terms list fetched', result.meta)); +}); + +const getOne = asyncHandler(async (req, res) => { + const data = await service.getDeliveryTermsById(req.params.id); + res.json(new ApiResponse(200, data, 'delivery_terms fetched')); +}); + +const update = asyncHandler(async (req, res) => { + const data = await service.updateDeliveryTerms(req.params.id, req.body, req.user?.id, req.id); + res.json(new ApiResponse(200, data, 'delivery_terms updated successfully')); +}); + +const remove = asyncHandler(async (req, res) => { + await service.deleteDeliveryTerms(req.params.id, req.user?.id, req.id); + res.json(new ApiResponse(200, null, 'delivery_terms deleted successfully')); +}); + +module.exports = { create, list, getOne, update, remove }; diff --git a/src/modules/masters/delivery-terms/delivery-terms.routes.js b/src/modules/masters/delivery-terms/delivery-terms.routes.js new file mode 100644 index 0000000..3b9374c --- /dev/null +++ b/src/modules/masters/delivery-terms/delivery-terms.routes.js @@ -0,0 +1,17 @@ +const express = require('express'); +const authenticate = require('../../../middlewares/auth.middleware'); +const authorize = require('../../../middlewares/rbac.middleware'); +const validate = require('../../../middlewares/validate.middleware'); +const controller = require('./delivery-terms.controller'); +const { createSchema, updateSchema, listQuerySchema } = require('./delivery-terms.validation'); + +const router = express.Router(); + +router.use(authenticate); +router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list); +router.get('/:id', authorize('MASTERS', 'view'), controller.getOne); +router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create); +router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update); +router.delete('/:id', authorize('MASTERS', 'delete'), controller.remove); + +module.exports = router; diff --git a/src/modules/masters/delivery-terms/delivery-terms.service.js b/src/modules/masters/delivery-terms/delivery-terms.service.js new file mode 100644 index 0000000..80c8d11 --- /dev/null +++ b/src/modules/masters/delivery-terms/delivery-terms.service.js @@ -0,0 +1,56 @@ +const { buildMasterService } = require('../_shared/master.factory'); + +const config = { + modelName: 'delivery_terms', + tableName: 'delivery_terms', + uniqueField: 'code', + softDelete: true, + fields: [ + { + name: 'code', + type: 'string', + uppercase: true, + searchable: true, + }, + { + name: 'name', + type: 'string', + uppercase: false, + searchable: true, + }, + { + name: 'description', + type: 'string', + uppercase: false, + searchable: false, + }, + { + name: 'is_active', + type: 'boolean', + uppercase: false, + searchable: false, + }, + { + name: 'created_by', + type: 'int', + uppercase: false, + searchable: false, + }, + { + name: 'updated_by', + type: 'int', + uppercase: false, + searchable: false, + }, + ], +}; + +const service = buildMasterService(config); + +module.exports = { + createDeliveryTerms: service.createOne, + listDeliveryTerms: service.list, + getDeliveryTermsById: service.getOne, + updateDeliveryTerms: service.updateOne, + deleteDeliveryTerms: service.removeOne, +}; diff --git a/src/modules/masters/delivery-terms/delivery-terms.validation.js b/src/modules/masters/delivery-terms/delivery-terms.validation.js new file mode 100644 index 0000000..f5b8222 --- /dev/null +++ b/src/modules/masters/delivery-terms/delivery-terms.validation.js @@ -0,0 +1,19 @@ +const Joi = require('joi'); + +const createSchema = Joi.object({ + code: Joi.string().allow('').optional(), + name: Joi.string().allow('').optional(), + description: Joi.string().allow('').optional(), + is_active: Joi.boolean().optional(), +}); + +const updateSchema = createSchema.min(1); + +const listQuerySchema = Joi.object({ + page: Joi.number().integer().min(1).default(1), + limit: Joi.number().integer().min(1).max(100).default(20), + search: Joi.string().allow('').optional(), + is_active: Joi.boolean().optional(), +}); + +module.exports = { createSchema, updateSchema, listQuerySchema }; diff --git a/src/modules/masters/departments/departments.controller.js b/src/modules/masters/departments/departments.controller.js new file mode 100644 index 0000000..2d99418 --- /dev/null +++ b/src/modules/masters/departments/departments.controller.js @@ -0,0 +1,30 @@ +const asyncHandler = require('../../../utils/asyncHandler'); +const ApiResponse = require('../../../utils/ApiResponse'); +const service = require('./departments.service'); + +const create = asyncHandler(async (req, res) => { + const data = await service.createDepartments(req.body, req.user?.id, req.id); + res.status(201).json(new ApiResponse(201, data, 'departments created successfully')); +}); + +const list = asyncHandler(async (req, res) => { + const result = await service.listDepartments(req.query); + res.json(new ApiResponse(200, result.data, 'departments list fetched', result.meta)); +}); + +const getOne = asyncHandler(async (req, res) => { + const data = await service.getDepartmentsById(req.params.id); + res.json(new ApiResponse(200, data, 'departments fetched')); +}); + +const update = asyncHandler(async (req, res) => { + const data = await service.updateDepartments(req.params.id, req.body, req.user?.id, req.id); + res.json(new ApiResponse(200, data, 'departments updated successfully')); +}); + +const remove = asyncHandler(async (req, res) => { + await service.deleteDepartments(req.params.id, req.user?.id, req.id); + res.json(new ApiResponse(200, null, 'departments deleted successfully')); +}); + +module.exports = { create, list, getOne, update, remove }; diff --git a/src/modules/masters/departments/departments.routes.js b/src/modules/masters/departments/departments.routes.js new file mode 100644 index 0000000..2d50fc1 --- /dev/null +++ b/src/modules/masters/departments/departments.routes.js @@ -0,0 +1,17 @@ +const express = require('express'); +const authenticate = require('../../../middlewares/auth.middleware'); +const authorize = require('../../../middlewares/rbac.middleware'); +const validate = require('../../../middlewares/validate.middleware'); +const controller = require('./departments.controller'); +const { createSchema, updateSchema, listQuerySchema } = require('./departments.validation'); + +const router = express.Router(); + +router.use(authenticate); +router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list); +router.get('/:id', authorize('MASTERS', 'view'), controller.getOne); +router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create); +router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update); +router.delete('/:id', authorize('MASTERS', 'delete'), controller.remove); + +module.exports = router; diff --git a/src/modules/masters/departments/departments.service.js b/src/modules/masters/departments/departments.service.js new file mode 100644 index 0000000..5cb2f24 --- /dev/null +++ b/src/modules/masters/departments/departments.service.js @@ -0,0 +1,44 @@ +const { buildMasterService } = require('../_shared/master.factory'); + +const config = { + modelName: 'departments', + tableName: 'departments', + uniqueField: 'name', + softDelete: true, + fields: [ + { + name: 'name', + type: 'string', + uppercase: false, + searchable: true, + }, + { + name: 'is_active', + type: 'boolean', + uppercase: false, + searchable: false, + }, + { + name: 'created_by', + type: 'int', + uppercase: false, + searchable: false, + }, + { + name: 'updated_by', + type: 'int', + uppercase: false, + searchable: false, + }, + ], +}; + +const service = buildMasterService(config); + +module.exports = { + createDepartments: service.createOne, + listDepartments: service.list, + getDepartmentsById: service.getOne, + updateDepartments: service.updateOne, + deleteDepartments: service.removeOne, +}; diff --git a/src/modules/masters/departments/departments.validation.js b/src/modules/masters/departments/departments.validation.js new file mode 100644 index 0000000..517a4d1 --- /dev/null +++ b/src/modules/masters/departments/departments.validation.js @@ -0,0 +1,17 @@ +const Joi = require('joi'); + +const createSchema = Joi.object({ + name: Joi.string().allow('').optional(), + is_active: Joi.boolean().optional(), +}); + +const updateSchema = createSchema.min(1); + +const listQuerySchema = Joi.object({ + page: Joi.number().integer().min(1).default(1), + limit: Joi.number().integer().min(1).max(100).default(20), + search: Joi.string().allow('').optional(), + is_active: Joi.boolean().optional(), +}); + +module.exports = { createSchema, updateSchema, listQuerySchema }; diff --git a/src/modules/masters/designations/designations.controller.js b/src/modules/masters/designations/designations.controller.js new file mode 100644 index 0000000..a425fad --- /dev/null +++ b/src/modules/masters/designations/designations.controller.js @@ -0,0 +1,30 @@ +const asyncHandler = require('../../../utils/asyncHandler'); +const ApiResponse = require('../../../utils/ApiResponse'); +const service = require('./designations.service'); + +const create = asyncHandler(async (req, res) => { + const data = await service.createDesignations(req.body, req.user?.id, req.id); + res.status(201).json(new ApiResponse(201, data, 'designations created successfully')); +}); + +const list = asyncHandler(async (req, res) => { + const result = await service.listDesignations(req.query); + res.json(new ApiResponse(200, result.data, 'designations list fetched', result.meta)); +}); + +const getOne = asyncHandler(async (req, res) => { + const data = await service.getDesignationsById(req.params.id); + res.json(new ApiResponse(200, data, 'designations fetched')); +}); + +const update = asyncHandler(async (req, res) => { + const data = await service.updateDesignations(req.params.id, req.body, req.user?.id, req.id); + res.json(new ApiResponse(200, data, 'designations updated successfully')); +}); + +const remove = asyncHandler(async (req, res) => { + await service.deleteDesignations(req.params.id, req.user?.id, req.id); + res.json(new ApiResponse(200, null, 'designations deleted successfully')); +}); + +module.exports = { create, list, getOne, update, remove }; diff --git a/src/modules/masters/designations/designations.routes.js b/src/modules/masters/designations/designations.routes.js new file mode 100644 index 0000000..76fc6e2 --- /dev/null +++ b/src/modules/masters/designations/designations.routes.js @@ -0,0 +1,17 @@ +const express = require('express'); +const authenticate = require('../../../middlewares/auth.middleware'); +const authorize = require('../../../middlewares/rbac.middleware'); +const validate = require('../../../middlewares/validate.middleware'); +const controller = require('./designations.controller'); +const { createSchema, updateSchema, listQuerySchema } = require('./designations.validation'); + +const router = express.Router(); + +router.use(authenticate); +router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list); +router.get('/:id', authorize('MASTERS', 'view'), controller.getOne); +router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create); +router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update); +router.delete('/:id', authorize('MASTERS', 'delete'), controller.remove); + +module.exports = router; diff --git a/src/modules/masters/designations/designations.service.js b/src/modules/masters/designations/designations.service.js new file mode 100644 index 0000000..b188ca1 --- /dev/null +++ b/src/modules/masters/designations/designations.service.js @@ -0,0 +1,44 @@ +const { buildMasterService } = require('../_shared/master.factory'); + +const config = { + modelName: 'designations', + tableName: 'designations', + uniqueField: 'name', + softDelete: true, + fields: [ + { + name: 'name', + type: 'string', + uppercase: false, + searchable: true, + }, + { + name: 'is_active', + type: 'boolean', + uppercase: false, + searchable: false, + }, + { + name: 'created_by', + type: 'int', + uppercase: false, + searchable: false, + }, + { + name: 'updated_by', + type: 'int', + uppercase: false, + searchable: false, + }, + ], +}; + +const service = buildMasterService(config); + +module.exports = { + createDesignations: service.createOne, + listDesignations: service.list, + getDesignationsById: service.getOne, + updateDesignations: service.updateOne, + deleteDesignations: service.removeOne, +}; diff --git a/src/modules/masters/designations/designations.validation.js b/src/modules/masters/designations/designations.validation.js new file mode 100644 index 0000000..517a4d1 --- /dev/null +++ b/src/modules/masters/designations/designations.validation.js @@ -0,0 +1,17 @@ +const Joi = require('joi'); + +const createSchema = Joi.object({ + name: Joi.string().allow('').optional(), + is_active: Joi.boolean().optional(), +}); + +const updateSchema = createSchema.min(1); + +const listQuerySchema = Joi.object({ + page: Joi.number().integer().min(1).default(1), + limit: Joi.number().integer().min(1).max(100).default(20), + search: Joi.string().allow('').optional(), + is_active: Joi.boolean().optional(), +}); + +module.exports = { createSchema, updateSchema, listQuerySchema }; diff --git a/src/modules/masters/document-series/document-series.controller.js b/src/modules/masters/document-series/document-series.controller.js new file mode 100644 index 0000000..208a753 --- /dev/null +++ b/src/modules/masters/document-series/document-series.controller.js @@ -0,0 +1,30 @@ +const asyncHandler = require('../../../utils/asyncHandler'); +const ApiResponse = require('../../../utils/ApiResponse'); +const service = require('./document-series.service'); + +const create = asyncHandler(async (req, res) => { + const data = await service.createDocumentSeries(req.body, req.user?.id, req.id); + res.status(201).json(new ApiResponse(201, data, 'document_series created successfully')); +}); + +const list = asyncHandler(async (req, res) => { + const result = await service.listDocumentSeries(req.query); + res.json(new ApiResponse(200, result.data, 'document_series list fetched', result.meta)); +}); + +const getOne = asyncHandler(async (req, res) => { + const data = await service.getDocumentSeriesById(req.params.id); + res.json(new ApiResponse(200, data, 'document_series fetched')); +}); + +const update = asyncHandler(async (req, res) => { + const data = await service.updateDocumentSeries(req.params.id, req.body, req.user?.id, req.id); + res.json(new ApiResponse(200, data, 'document_series updated successfully')); +}); + +const remove = asyncHandler(async (req, res) => { + await service.deleteDocumentSeries(req.params.id, req.user?.id, req.id); + res.json(new ApiResponse(200, null, 'document_series deleted successfully')); +}); + +module.exports = { create, list, getOne, update, remove }; diff --git a/src/modules/masters/document-series/document-series.routes.js b/src/modules/masters/document-series/document-series.routes.js new file mode 100644 index 0000000..6afde95 --- /dev/null +++ b/src/modules/masters/document-series/document-series.routes.js @@ -0,0 +1,17 @@ +const express = require('express'); +const authenticate = require('../../../middlewares/auth.middleware'); +const authorize = require('../../../middlewares/rbac.middleware'); +const validate = require('../../../middlewares/validate.middleware'); +const controller = require('./document-series.controller'); +const { createSchema, updateSchema, listQuerySchema } = require('./document-series.validation'); + +const router = express.Router(); + +router.use(authenticate); +router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list); +router.get('/:id', authorize('MASTERS', 'view'), controller.getOne); +router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create); +router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update); +router.delete('/:id', authorize('MASTERS', 'delete'), controller.remove); + +module.exports = router; diff --git a/src/modules/masters/document-series/document-series.service.js b/src/modules/masters/document-series/document-series.service.js new file mode 100644 index 0000000..1730686 --- /dev/null +++ b/src/modules/masters/document-series/document-series.service.js @@ -0,0 +1,68 @@ +const { buildMasterService } = require('../_shared/master.factory'); + +const config = { + modelName: 'document_series', + tableName: 'document_series', + uniqueField: 'code', + softDelete: false, + fields: [ + { + name: 'code', + type: 'string', + uppercase: true, + searchable: true, + }, + { + name: 'prefix', + type: 'string', + uppercase: false, + searchable: true, + }, + { + name: 'current_number', + type: 'int', + uppercase: false, + searchable: false, + }, + { + name: 'padding', + type: 'int', + uppercase: false, + searchable: false, + }, + { + name: 'description', + type: 'string', + uppercase: false, + searchable: false, + }, + { + name: 'is_active', + type: 'boolean', + uppercase: false, + searchable: false, + }, + { + name: 'created_by', + type: 'int', + uppercase: false, + searchable: false, + }, + { + name: 'updated_by', + type: 'int', + uppercase: false, + searchable: false, + }, + ], +}; + +const service = buildMasterService(config); + +module.exports = { + createDocumentSeries: service.createOne, + listDocumentSeries: service.list, + getDocumentSeriesById: service.getOne, + updateDocumentSeries: service.updateOne, + deleteDocumentSeries: service.removeOne, +}; diff --git a/src/modules/masters/document-series/document-series.validation.js b/src/modules/masters/document-series/document-series.validation.js new file mode 100644 index 0000000..bfcf865 --- /dev/null +++ b/src/modules/masters/document-series/document-series.validation.js @@ -0,0 +1,21 @@ +const Joi = require('joi'); + +const createSchema = Joi.object({ + code: Joi.string().allow('').optional(), + prefix: Joi.string().allow('').optional(), + current_number: Joi.number().integer().optional(), + padding: Joi.number().integer().optional(), + description: Joi.string().allow('').optional(), + is_active: Joi.boolean().optional(), +}); + +const updateSchema = createSchema.min(1); + +const listQuerySchema = Joi.object({ + page: Joi.number().integer().min(1).default(1), + limit: Joi.number().integer().min(1).max(100).default(20), + search: Joi.string().allow('').optional(), + is_active: Joi.boolean().optional(), +}); + +module.exports = { createSchema, updateSchema, listQuerySchema }; diff --git a/src/modules/masters/gst-rates/gst-rates.controller.js b/src/modules/masters/gst-rates/gst-rates.controller.js new file mode 100644 index 0000000..d36f7f0 --- /dev/null +++ b/src/modules/masters/gst-rates/gst-rates.controller.js @@ -0,0 +1,30 @@ +const asyncHandler = require('../../../utils/asyncHandler'); +const ApiResponse = require('../../../utils/ApiResponse'); +const service = require('./gst-rates.service'); + +const create = asyncHandler(async (req, res) => { + const data = await service.createGstRates(req.body, req.user?.id, req.id); + res.status(201).json(new ApiResponse(201, data, 'gst_rates created successfully')); +}); + +const list = asyncHandler(async (req, res) => { + const result = await service.listGstRates(req.query); + res.json(new ApiResponse(200, result.data, 'gst_rates list fetched', result.meta)); +}); + +const getOne = asyncHandler(async (req, res) => { + const data = await service.getGstRatesById(req.params.id); + res.json(new ApiResponse(200, data, 'gst_rates fetched')); +}); + +const update = asyncHandler(async (req, res) => { + const data = await service.updateGstRates(req.params.id, req.body, req.user?.id, req.id); + res.json(new ApiResponse(200, data, 'gst_rates updated successfully')); +}); + +const remove = asyncHandler(async (req, res) => { + await service.deleteGstRates(req.params.id, req.user?.id, req.id); + res.json(new ApiResponse(200, null, 'gst_rates deleted successfully')); +}); + +module.exports = { create, list, getOne, update, remove }; diff --git a/src/modules/masters/gst-rates/gst-rates.routes.js b/src/modules/masters/gst-rates/gst-rates.routes.js new file mode 100644 index 0000000..f56b51d --- /dev/null +++ b/src/modules/masters/gst-rates/gst-rates.routes.js @@ -0,0 +1,17 @@ +const express = require('express'); +const authenticate = require('../../../middlewares/auth.middleware'); +const authorize = require('../../../middlewares/rbac.middleware'); +const validate = require('../../../middlewares/validate.middleware'); +const controller = require('./gst-rates.controller'); +const { createSchema, updateSchema, listQuerySchema } = require('./gst-rates.validation'); + +const router = express.Router(); + +router.use(authenticate); +router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list); +router.get('/:id', authorize('MASTERS', 'view'), controller.getOne); +router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create); +router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update); +router.delete('/:id', authorize('MASTERS', 'delete'), controller.remove); + +module.exports = router; diff --git a/src/modules/masters/gst-rates/gst-rates.service.js b/src/modules/masters/gst-rates/gst-rates.service.js new file mode 100644 index 0000000..b617957 --- /dev/null +++ b/src/modules/masters/gst-rates/gst-rates.service.js @@ -0,0 +1,38 @@ +const { buildMasterService } = require('../_shared/master.factory'); + +const config = { + modelName: 'gst_rates', + tableName: 'gst_rates', + uniqueField: 'rate_pct', + softDelete: false, + fields: [ + { + name: 'rate_pct', + type: 'decimal', + uppercase: false, + searchable: false, + }, + { + name: 'description', + type: 'string', + uppercase: false, + searchable: true, + }, + { + name: 'is_active', + type: 'boolean', + uppercase: false, + searchable: false, + }, + ], +}; + +const service = buildMasterService(config); + +module.exports = { + createGstRates: service.createOne, + listGstRates: service.list, + getGstRatesById: service.getOne, + updateGstRates: service.updateOne, + deleteGstRates: service.removeOne, +}; diff --git a/src/modules/masters/gst-rates/gst-rates.validation.js b/src/modules/masters/gst-rates/gst-rates.validation.js new file mode 100644 index 0000000..27a3b0c --- /dev/null +++ b/src/modules/masters/gst-rates/gst-rates.validation.js @@ -0,0 +1,18 @@ +const Joi = require('joi'); + +const createSchema = Joi.object({ + rate_pct: Joi.number().precision(2).optional(), + description: Joi.string().allow('').optional(), + is_active: Joi.boolean().optional(), +}); + +const updateSchema = createSchema.min(1); + +const listQuerySchema = Joi.object({ + page: Joi.number().integer().min(1).default(1), + limit: Joi.number().integer().min(1).max(100).default(20), + search: Joi.string().allow('').optional(), + is_active: Joi.boolean().optional(), +}); + +module.exports = { createSchema, updateSchema, listQuerySchema }; diff --git a/src/modules/masters/index.js b/src/modules/masters/index.js new file mode 100644 index 0000000..1ea39d3 --- /dev/null +++ b/src/modules/masters/index.js @@ -0,0 +1,20 @@ +const express = require('express'); + +const router = express.Router(); + +router.use('/uom', require('./uom/uom.routes')); +router.use('/item-categories', require('./item-categories/item-categories.routes')); +router.use('/brands', require('./brands/brands.routes')); +router.use('/gst-rates', require('./gst-rates/gst-rates.routes')); +router.use('/payment-terms', require('./payment-terms/payment-terms.routes')); +router.use('/delivery-terms', require('./delivery-terms/delivery-terms.routes')); +router.use('/asset-categories', require('./asset-categories/asset-categories.routes')); +router.use('/departments', require('./departments/departments.routes')); +router.use('/designations', require('./designations/designations.routes')); +router.use('/document-series', require('./document-series/document-series.routes')); +router.use('/item-subcategories', require('./item-subcategories/item-subcategories.routes')); +router.use('/items', require('./items/items.routes')); +router.use('/warehouses', require('./warehouses/warehouses.routes')); +router.use('/plants', require('./plants/plants.routes')); + +module.exports = router; diff --git a/src/modules/masters/item-categories/item-categories.controller.js b/src/modules/masters/item-categories/item-categories.controller.js new file mode 100644 index 0000000..01ebf72 --- /dev/null +++ b/src/modules/masters/item-categories/item-categories.controller.js @@ -0,0 +1,30 @@ +const asyncHandler = require('../../../utils/asyncHandler'); +const ApiResponse = require('../../../utils/ApiResponse'); +const service = require('./item-categories.service'); + +const create = asyncHandler(async (req, res) => { + const data = await service.createItemCategories(req.body, req.user?.id, req.id); + res.status(201).json(new ApiResponse(201, data, 'item_categories created successfully')); +}); + +const list = asyncHandler(async (req, res) => { + const result = await service.listItemCategories(req.query); + res.json(new ApiResponse(200, result.data, 'item_categories list fetched', result.meta)); +}); + +const getOne = asyncHandler(async (req, res) => { + const data = await service.getItemCategoriesById(req.params.id); + res.json(new ApiResponse(200, data, 'item_categories fetched')); +}); + +const update = asyncHandler(async (req, res) => { + const data = await service.updateItemCategories(req.params.id, req.body, req.user?.id, req.id); + res.json(new ApiResponse(200, data, 'item_categories updated successfully')); +}); + +const remove = asyncHandler(async (req, res) => { + await service.deleteItemCategories(req.params.id, req.user?.id, req.id); + res.json(new ApiResponse(200, null, 'item_categories deleted successfully')); +}); + +module.exports = { create, list, getOne, update, remove }; diff --git a/src/modules/masters/item-categories/item-categories.routes.js b/src/modules/masters/item-categories/item-categories.routes.js new file mode 100644 index 0000000..5e09cde --- /dev/null +++ b/src/modules/masters/item-categories/item-categories.routes.js @@ -0,0 +1,17 @@ +const express = require('express'); +const authenticate = require('../../../middlewares/auth.middleware'); +const authorize = require('../../../middlewares/rbac.middleware'); +const validate = require('../../../middlewares/validate.middleware'); +const controller = require('./item-categories.controller'); +const { createSchema, updateSchema, listQuerySchema } = require('./item-categories.validation'); + +const router = express.Router(); + +router.use(authenticate); +router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list); +router.get('/:id', authorize('MASTERS', 'view'), controller.getOne); +router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create); +router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update); +router.delete('/:id', authorize('MASTERS', 'delete'), controller.remove); + +module.exports = router; diff --git a/src/modules/masters/item-categories/item-categories.service.js b/src/modules/masters/item-categories/item-categories.service.js new file mode 100644 index 0000000..10844ad --- /dev/null +++ b/src/modules/masters/item-categories/item-categories.service.js @@ -0,0 +1,50 @@ +const { buildMasterService } = require('../_shared/master.factory'); + +const config = { + modelName: 'item_categories', + tableName: 'item_categories', + uniqueField: 'code', + softDelete: true, + fields: [ + { + name: 'code', + type: 'string', + uppercase: true, + searchable: true, + }, + { + name: 'name', + type: 'string', + uppercase: false, + searchable: true, + }, + { + name: 'is_active', + type: 'boolean', + uppercase: false, + searchable: false, + }, + { + name: 'created_by', + type: 'int', + uppercase: false, + searchable: false, + }, + { + name: 'updated_by', + type: 'int', + uppercase: false, + searchable: false, + }, + ], +}; + +const service = buildMasterService(config); + +module.exports = { + createItemCategories: service.createOne, + listItemCategories: service.list, + getItemCategoriesById: service.getOne, + updateItemCategories: service.updateOne, + deleteItemCategories: service.removeOne, +}; diff --git a/src/modules/masters/item-categories/item-categories.validation.js b/src/modules/masters/item-categories/item-categories.validation.js new file mode 100644 index 0000000..2a546f6 --- /dev/null +++ b/src/modules/masters/item-categories/item-categories.validation.js @@ -0,0 +1,18 @@ +const Joi = require('joi'); + +const createSchema = Joi.object({ + code: Joi.string().allow('').optional(), + name: Joi.string().allow('').optional(), + is_active: Joi.boolean().optional(), +}); + +const updateSchema = createSchema.min(1); + +const listQuerySchema = Joi.object({ + page: Joi.number().integer().min(1).default(1), + limit: Joi.number().integer().min(1).max(100).default(20), + search: Joi.string().allow('').optional(), + is_active: Joi.boolean().optional(), +}); + +module.exports = { createSchema, updateSchema, listQuerySchema }; diff --git a/src/modules/masters/item-subcategories/item-subcategories.controller.js b/src/modules/masters/item-subcategories/item-subcategories.controller.js new file mode 100644 index 0000000..b831061 --- /dev/null +++ b/src/modules/masters/item-subcategories/item-subcategories.controller.js @@ -0,0 +1,30 @@ +const asyncHandler = require('../../../utils/asyncHandler'); +const ApiResponse = require('../../../utils/ApiResponse'); +const service = require('./item-subcategories.service'); + +const create = asyncHandler(async (req, res) => { + const data = await service.createItemSubcategories(req.body, req.user?.id, req.id); + res.status(201).json(new ApiResponse(201, data, 'item_subcategories created successfully')); +}); + +const list = asyncHandler(async (req, res) => { + const result = await service.listItemSubcategories(req.query); + res.json(new ApiResponse(200, result.data, 'item_subcategories list fetched', result.meta)); +}); + +const getOne = asyncHandler(async (req, res) => { + const data = await service.getItemSubcategoriesById(req.params.id); + res.json(new ApiResponse(200, data, 'item_subcategories fetched')); +}); + +const update = asyncHandler(async (req, res) => { + const data = await service.updateItemSubcategories(req.params.id, req.body, req.user?.id, req.id); + res.json(new ApiResponse(200, data, 'item_subcategories updated successfully')); +}); + +const remove = asyncHandler(async (req, res) => { + await service.deleteItemSubcategories(req.params.id, req.user?.id, req.id); + res.json(new ApiResponse(200, null, 'item_subcategories deleted successfully')); +}); + +module.exports = { create, list, getOne, update, remove }; diff --git a/src/modules/masters/item-subcategories/item-subcategories.routes.js b/src/modules/masters/item-subcategories/item-subcategories.routes.js new file mode 100644 index 0000000..f811e46 --- /dev/null +++ b/src/modules/masters/item-subcategories/item-subcategories.routes.js @@ -0,0 +1,17 @@ +const express = require('express'); +const authenticate = require('../../../middlewares/auth.middleware'); +const authorize = require('../../../middlewares/rbac.middleware'); +const validate = require('../../../middlewares/validate.middleware'); +const controller = require('./item-subcategories.controller'); +const { createSchema, updateSchema, listQuerySchema } = require('./item-subcategories.validation'); + +const router = express.Router(); + +router.use(authenticate); +router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list); +router.get('/:id', authorize('MASTERS', 'view'), controller.getOne); +router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create); +router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update); +router.delete('/:id', authorize('MASTERS', 'delete'), controller.remove); + +module.exports = router; diff --git a/src/modules/masters/item-subcategories/item-subcategories.service.js b/src/modules/masters/item-subcategories/item-subcategories.service.js new file mode 100644 index 0000000..0a659c9 --- /dev/null +++ b/src/modules/masters/item-subcategories/item-subcategories.service.js @@ -0,0 +1,56 @@ +const { buildMasterService } = require('../_shared/master.factory'); + +const config = { + modelName: 'item_subcategories', + tableName: 'item_subcategories', + uniqueField: 'code', + softDelete: true, + fields: [ + { + name: 'item_category_id', + type: 'int', + uppercase: false, + searchable: false, + }, + { + name: 'code', + type: 'string', + uppercase: true, + searchable: true, + }, + { + name: 'name', + type: 'string', + uppercase: false, + searchable: true, + }, + { + name: 'is_active', + type: 'boolean', + uppercase: false, + searchable: false, + }, + { + name: 'created_by', + type: 'int', + uppercase: false, + searchable: false, + }, + { + name: 'updated_by', + type: 'int', + uppercase: false, + searchable: false, + }, + ], +}; + +const service = buildMasterService(config); + +module.exports = { + createItemSubcategories: service.createOne, + listItemSubcategories: service.list, + getItemSubcategoriesById: service.getOne, + updateItemSubcategories: service.updateOne, + deleteItemSubcategories: service.removeOne, +}; diff --git a/src/modules/masters/item-subcategories/item-subcategories.validation.js b/src/modules/masters/item-subcategories/item-subcategories.validation.js new file mode 100644 index 0000000..6eb3cdb --- /dev/null +++ b/src/modules/masters/item-subcategories/item-subcategories.validation.js @@ -0,0 +1,19 @@ +const Joi = require('joi'); + +const createSchema = Joi.object({ + item_category_id: Joi.number().integer().optional(), + code: Joi.string().allow('').optional(), + name: Joi.string().allow('').optional(), + is_active: Joi.boolean().optional(), +}); + +const updateSchema = createSchema.min(1); + +const listQuerySchema = Joi.object({ + page: Joi.number().integer().min(1).default(1), + limit: Joi.number().integer().min(1).max(100).default(20), + search: Joi.string().allow('').optional(), + is_active: Joi.boolean().optional(), +}); + +module.exports = { createSchema, updateSchema, listQuerySchema }; diff --git a/src/modules/masters/items/items.controller.js b/src/modules/masters/items/items.controller.js new file mode 100644 index 0000000..127fb8b --- /dev/null +++ b/src/modules/masters/items/items.controller.js @@ -0,0 +1,30 @@ +const asyncHandler = require('../../../utils/asyncHandler'); +const ApiResponse = require('../../../utils/ApiResponse'); +const service = require('./items.service'); + +const create = asyncHandler(async (req, res) => { + const data = await service.createItems(req.body, req.user?.id, req.id); + res.status(201).json(new ApiResponse(201, data, 'items created successfully')); +}); + +const list = asyncHandler(async (req, res) => { + const result = await service.listItems(req.query); + res.json(new ApiResponse(200, result.data, 'items list fetched', result.meta)); +}); + +const getOne = asyncHandler(async (req, res) => { + const data = await service.getItemsById(req.params.id); + res.json(new ApiResponse(200, data, 'items fetched')); +}); + +const update = asyncHandler(async (req, res) => { + const data = await service.updateItems(req.params.id, req.body, req.user?.id, req.id); + res.json(new ApiResponse(200, data, 'items updated successfully')); +}); + +const remove = asyncHandler(async (req, res) => { + await service.deleteItems(req.params.id, req.user?.id, req.id); + res.json(new ApiResponse(200, null, 'items deleted successfully')); +}); + +module.exports = { create, list, getOne, update, remove }; diff --git a/src/modules/masters/items/items.routes.js b/src/modules/masters/items/items.routes.js new file mode 100644 index 0000000..a1cd828 --- /dev/null +++ b/src/modules/masters/items/items.routes.js @@ -0,0 +1,17 @@ +const express = require('express'); +const authenticate = require('../../../middlewares/auth.middleware'); +const authorize = require('../../../middlewares/rbac.middleware'); +const validate = require('../../../middlewares/validate.middleware'); +const controller = require('./items.controller'); +const { createSchema, updateSchema, listQuerySchema } = require('./items.validation'); + +const router = express.Router(); + +router.use(authenticate); +router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list); +router.get('/:id', authorize('MASTERS', 'view'), controller.getOne); +router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create); +router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update); +router.delete('/:id', authorize('MASTERS', 'delete'), controller.remove); + +module.exports = router; diff --git a/src/modules/masters/items/items.service.js b/src/modules/masters/items/items.service.js new file mode 100644 index 0000000..e669cb5 --- /dev/null +++ b/src/modules/masters/items/items.service.js @@ -0,0 +1,116 @@ +const { buildMasterService } = require('../_shared/master.factory'); + +const config = { + modelName: 'items', + tableName: 'items', + uniqueField: 'item_code', + softDelete: true, + fields: [ + { + name: 'item_code', + type: 'string', + uppercase: true, + searchable: true, + }, + { + name: 'item_name', + type: 'string', + uppercase: false, + searchable: true, + }, + { + name: 'item_category_id', + type: 'int', + uppercase: false, + searchable: false, + }, + { + name: 'item_subcategory_id', + type: 'int', + uppercase: false, + searchable: false, + }, + { + name: 'uom_id', + type: 'int', + uppercase: false, + searchable: false, + }, + { + name: 'hsn_code_id', + type: 'int', + uppercase: false, + searchable: false, + }, + { + name: 'gst_rate_id', + type: 'int', + uppercase: false, + searchable: false, + }, + { + name: 'brand_id', + type: 'int', + uppercase: false, + searchable: false, + }, + { + name: 'is_asset_item', + type: 'boolean', + uppercase: false, + searchable: false, + }, + { + name: 'description', + type: 'string', + uppercase: false, + searchable: false, + }, + { + name: 'specification', + type: 'string', + uppercase: false, + searchable: false, + }, + { + name: 'min_order_qty', + type: 'decimal', + uppercase: false, + searchable: false, + }, + { + name: 'reorder_level', + type: 'decimal', + uppercase: false, + searchable: false, + }, + { + name: 'is_active', + type: 'boolean', + uppercase: false, + searchable: false, + }, + { + name: 'created_by', + type: 'int', + uppercase: false, + searchable: false, + }, + { + name: 'updated_by', + type: 'int', + uppercase: false, + searchable: false, + }, + ], +}; + +const service = buildMasterService(config); + +module.exports = { + createItems: service.createOne, + listItems: service.list, + getItemsById: service.getOne, + updateItems: service.updateOne, + deleteItems: service.removeOne, +}; diff --git a/src/modules/masters/items/items.validation.js b/src/modules/masters/items/items.validation.js new file mode 100644 index 0000000..0427146 --- /dev/null +++ b/src/modules/masters/items/items.validation.js @@ -0,0 +1,29 @@ +const Joi = require('joi'); + +const createSchema = Joi.object({ + item_code: Joi.string().allow('').optional(), + item_name: Joi.string().allow('').optional(), + item_category_id: Joi.number().integer().optional(), + item_subcategory_id: Joi.number().integer().optional(), + uom_id: Joi.number().integer().optional(), + hsn_code_id: Joi.number().integer().optional(), + gst_rate_id: Joi.number().integer().optional(), + brand_id: Joi.number().integer().optional(), + is_asset_item: Joi.boolean().optional(), + description: Joi.string().allow('').optional(), + specification: Joi.string().allow('').optional(), + min_order_qty: Joi.number().precision(4).optional(), + reorder_level: Joi.number().precision(4).optional(), + is_active: Joi.boolean().optional(), +}); + +const updateSchema = createSchema.min(1); + +const listQuerySchema = Joi.object({ + page: Joi.number().integer().min(1).default(1), + limit: Joi.number().integer().min(1).max(100).default(20), + search: Joi.string().allow('').optional(), + is_active: Joi.boolean().optional(), +}); + +module.exports = { createSchema, updateSchema, listQuerySchema }; diff --git a/src/modules/masters/payment-terms/payment-terms.controller.js b/src/modules/masters/payment-terms/payment-terms.controller.js new file mode 100644 index 0000000..6db8d51 --- /dev/null +++ b/src/modules/masters/payment-terms/payment-terms.controller.js @@ -0,0 +1,30 @@ +const asyncHandler = require('../../../utils/asyncHandler'); +const ApiResponse = require('../../../utils/ApiResponse'); +const service = require('./payment-terms.service'); + +const create = asyncHandler(async (req, res) => { + const data = await service.createPaymentTerms(req.body, req.user?.id, req.id); + res.status(201).json(new ApiResponse(201, data, 'payment_terms created successfully')); +}); + +const list = asyncHandler(async (req, res) => { + const result = await service.listPaymentTerms(req.query); + res.json(new ApiResponse(200, result.data, 'payment_terms list fetched', result.meta)); +}); + +const getOne = asyncHandler(async (req, res) => { + const data = await service.getPaymentTermsById(req.params.id); + res.json(new ApiResponse(200, data, 'payment_terms fetched')); +}); + +const update = asyncHandler(async (req, res) => { + const data = await service.updatePaymentTerms(req.params.id, req.body, req.user?.id, req.id); + res.json(new ApiResponse(200, data, 'payment_terms updated successfully')); +}); + +const remove = asyncHandler(async (req, res) => { + await service.deletePaymentTerms(req.params.id, req.user?.id, req.id); + res.json(new ApiResponse(200, null, 'payment_terms deleted successfully')); +}); + +module.exports = { create, list, getOne, update, remove }; diff --git a/src/modules/masters/payment-terms/payment-terms.routes.js b/src/modules/masters/payment-terms/payment-terms.routes.js new file mode 100644 index 0000000..0e84025 --- /dev/null +++ b/src/modules/masters/payment-terms/payment-terms.routes.js @@ -0,0 +1,17 @@ +const express = require('express'); +const authenticate = require('../../../middlewares/auth.middleware'); +const authorize = require('../../../middlewares/rbac.middleware'); +const validate = require('../../../middlewares/validate.middleware'); +const controller = require('./payment-terms.controller'); +const { createSchema, updateSchema, listQuerySchema } = require('./payment-terms.validation'); + +const router = express.Router(); + +router.use(authenticate); +router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list); +router.get('/:id', authorize('MASTERS', 'view'), controller.getOne); +router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create); +router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update); +router.delete('/:id', authorize('MASTERS', 'delete'), controller.remove); + +module.exports = router; diff --git a/src/modules/masters/payment-terms/payment-terms.service.js b/src/modules/masters/payment-terms/payment-terms.service.js new file mode 100644 index 0000000..78f3a84 --- /dev/null +++ b/src/modules/masters/payment-terms/payment-terms.service.js @@ -0,0 +1,62 @@ +const { buildMasterService } = require('../_shared/master.factory'); + +const config = { + modelName: 'payment_terms', + tableName: 'payment_terms', + uniqueField: 'code', + softDelete: true, + fields: [ + { + name: 'code', + type: 'string', + uppercase: true, + searchable: true, + }, + { + name: 'name', + type: 'string', + uppercase: false, + searchable: true, + }, + { + name: 'credit_days', + type: 'int', + uppercase: false, + searchable: false, + }, + { + name: 'description', + type: 'string', + uppercase: false, + searchable: false, + }, + { + name: 'is_active', + type: 'boolean', + uppercase: false, + searchable: false, + }, + { + name: 'created_by', + type: 'int', + uppercase: false, + searchable: false, + }, + { + name: 'updated_by', + type: 'int', + uppercase: false, + searchable: false, + }, + ], +}; + +const service = buildMasterService(config); + +module.exports = { + createPaymentTerms: service.createOne, + listPaymentTerms: service.list, + getPaymentTermsById: service.getOne, + updatePaymentTerms: service.updateOne, + deletePaymentTerms: service.removeOne, +}; diff --git a/src/modules/masters/payment-terms/payment-terms.validation.js b/src/modules/masters/payment-terms/payment-terms.validation.js new file mode 100644 index 0000000..f0d4947 --- /dev/null +++ b/src/modules/masters/payment-terms/payment-terms.validation.js @@ -0,0 +1,20 @@ +const Joi = require('joi'); + +const createSchema = Joi.object({ + code: Joi.string().allow('').optional(), + name: Joi.string().allow('').optional(), + credit_days: Joi.number().integer().optional(), + description: Joi.string().allow('').optional(), + is_active: Joi.boolean().optional(), +}); + +const updateSchema = createSchema.min(1); + +const listQuerySchema = Joi.object({ + page: Joi.number().integer().min(1).default(1), + limit: Joi.number().integer().min(1).max(100).default(20), + search: Joi.string().allow('').optional(), + is_active: Joi.boolean().optional(), +}); + +module.exports = { createSchema, updateSchema, listQuerySchema }; diff --git a/src/modules/masters/plants/plants.controller.js b/src/modules/masters/plants/plants.controller.js new file mode 100644 index 0000000..b66e623 --- /dev/null +++ b/src/modules/masters/plants/plants.controller.js @@ -0,0 +1,30 @@ +const asyncHandler = require('../../../utils/asyncHandler'); +const ApiResponse = require('../../../utils/ApiResponse'); +const service = require('./plants.service'); + +const create = asyncHandler(async (req, res) => { + const data = await service.createPlants(req.body, req.user?.id, req.id); + res.status(201).json(new ApiResponse(201, data, 'plants created successfully')); +}); + +const list = asyncHandler(async (req, res) => { + const result = await service.listPlants(req.query); + res.json(new ApiResponse(200, result.data, 'plants list fetched', result.meta)); +}); + +const getOne = asyncHandler(async (req, res) => { + const data = await service.getPlantsById(req.params.id); + res.json(new ApiResponse(200, data, 'plants fetched')); +}); + +const update = asyncHandler(async (req, res) => { + const data = await service.updatePlants(req.params.id, req.body, req.user?.id, req.id); + res.json(new ApiResponse(200, data, 'plants updated successfully')); +}); + +const remove = asyncHandler(async (req, res) => { + await service.deletePlants(req.params.id, req.user?.id, req.id); + res.json(new ApiResponse(200, null, 'plants deleted successfully')); +}); + +module.exports = { create, list, getOne, update, remove }; diff --git a/src/modules/masters/plants/plants.routes.js b/src/modules/masters/plants/plants.routes.js new file mode 100644 index 0000000..58197b2 --- /dev/null +++ b/src/modules/masters/plants/plants.routes.js @@ -0,0 +1,17 @@ +const express = require('express'); +const authenticate = require('../../../middlewares/auth.middleware'); +const authorize = require('../../../middlewares/rbac.middleware'); +const validate = require('../../../middlewares/validate.middleware'); +const controller = require('./plants.controller'); +const { createSchema, updateSchema, listQuerySchema } = require('./plants.validation'); + +const router = express.Router(); + +router.use(authenticate); +router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list); +router.get('/:id', authorize('MASTERS', 'view'), controller.getOne); +router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create); +router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update); +router.delete('/:id', authorize('MASTERS', 'delete'), controller.remove); + +module.exports = router; diff --git a/src/modules/masters/plants/plants.service.js b/src/modules/masters/plants/plants.service.js new file mode 100644 index 0000000..5d69b65 --- /dev/null +++ b/src/modules/masters/plants/plants.service.js @@ -0,0 +1,86 @@ +const { buildMasterService } = require('../_shared/master.factory'); + +const config = { + modelName: 'plants', + tableName: 'plants', + uniqueField: 'code', + softDelete: true, + fields: [ + { + name: 'code', + type: 'string', + uppercase: true, + searchable: true, + }, + { + name: 'name', + type: 'string', + uppercase: false, + searchable: true, + }, + { + name: 'gstin', + type: 'string', + uppercase: false, + searchable: false, + }, + { + name: 'address', + type: 'string', + uppercase: false, + searchable: false, + }, + { + name: 'city', + type: 'string', + uppercase: false, + searchable: false, + }, + { + name: 'state', + type: 'string', + uppercase: false, + searchable: false, + }, + { + name: 'pincode', + type: 'string', + uppercase: false, + searchable: false, + }, + { + name: 'phone', + type: 'string', + uppercase: false, + searchable: false, + }, + { + name: 'is_active', + type: 'boolean', + uppercase: false, + searchable: false, + }, + { + name: 'created_by', + type: 'int', + uppercase: false, + searchable: false, + }, + { + name: 'updated_by', + type: 'int', + uppercase: false, + searchable: false, + }, + ], +}; + +const service = buildMasterService(config); + +module.exports = { + createPlants: service.createOne, + listPlants: service.list, + getPlantsById: service.getOne, + updatePlants: service.updateOne, + deletePlants: service.removeOne, +}; diff --git a/src/modules/masters/plants/plants.validation.js b/src/modules/masters/plants/plants.validation.js new file mode 100644 index 0000000..eb45dc7 --- /dev/null +++ b/src/modules/masters/plants/plants.validation.js @@ -0,0 +1,24 @@ +const Joi = require('joi'); + +const createSchema = Joi.object({ + code: Joi.string().allow('').optional(), + name: Joi.string().allow('').optional(), + gstin: Joi.string().allow('').optional(), + address: Joi.string().allow('').optional(), + city: Joi.string().allow('').optional(), + state: Joi.string().allow('').optional(), + pincode: Joi.string().allow('').optional(), + phone: Joi.string().allow('').optional(), + is_active: Joi.boolean().optional(), +}); + +const updateSchema = createSchema.min(1); + +const listQuerySchema = Joi.object({ + page: Joi.number().integer().min(1).default(1), + limit: Joi.number().integer().min(1).max(100).default(20), + search: Joi.string().allow('').optional(), + is_active: Joi.boolean().optional(), +}); + +module.exports = { createSchema, updateSchema, listQuerySchema }; diff --git a/src/modules/masters/uom/uom.controller.js b/src/modules/masters/uom/uom.controller.js new file mode 100644 index 0000000..53ed9f5 --- /dev/null +++ b/src/modules/masters/uom/uom.controller.js @@ -0,0 +1,30 @@ +const asyncHandler = require('../../../utils/asyncHandler'); +const ApiResponse = require('../../../utils/ApiResponse'); +const service = require('./uom.service'); + +const create = asyncHandler(async (req, res) => { + const data = await service.createUom(req.body, req.user?.id, req.id); + res.status(201).json(new ApiResponse(201, data, 'UOM created successfully')); +}); + +const list = asyncHandler(async (req, res) => { + const result = await service.listUom(req.query); + res.json(new ApiResponse(200, result.data, 'UOM list fetched', result.meta)); +}); + +const getOne = asyncHandler(async (req, res) => { + const data = await service.getUomById(req.params.id); + res.json(new ApiResponse(200, data, 'UOM fetched')); +}); + +const update = asyncHandler(async (req, res) => { + const data = await service.updateUom(req.params.id, req.body, req.user?.id, req.id); + res.json(new ApiResponse(200, data, 'UOM updated successfully')); +}); + +const remove = asyncHandler(async (req, res) => { + await service.deleteUom(req.params.id, req.user?.id, req.id); + res.json(new ApiResponse(200, null, 'UOM deleted successfully')); +}); + +module.exports = { create, list, getOne, update, remove }; diff --git a/src/modules/masters/uom/uom.routes.js b/src/modules/masters/uom/uom.routes.js new file mode 100644 index 0000000..cfd6642 --- /dev/null +++ b/src/modules/masters/uom/uom.routes.js @@ -0,0 +1,23 @@ +const express = require('express'); +const authenticate = require('../../../middlewares/auth.middleware'); +const authorize = require('../../../middlewares/rbac.middleware'); +const validate = require('../../../middlewares/validate.middleware'); +const controller = require('./uom.controller'); +const { createUomSchema, updateUomSchema, listUomQuerySchema } = require('./uom.validation'); + +const router = express.Router(); + +router.use(authenticate); + +router.get( + '/', + authorize('MASTERS', 'view'), + validate(listUomQuerySchema, 'query'), + controller.list +); +router.get('/:id', authorize('MASTERS', 'view'), controller.getOne); +router.post('/', authorize('MASTERS', 'create'), validate(createUomSchema), controller.create); +router.put('/:id', authorize('MASTERS', 'edit'), validate(updateUomSchema), controller.update); +router.delete('/:id', authorize('MASTERS', 'delete'), controller.remove); + +module.exports = router; diff --git a/src/modules/masters/uom/uom.service.js b/src/modules/masters/uom/uom.service.js new file mode 100644 index 0000000..7b4df77 --- /dev/null +++ b/src/modules/masters/uom/uom.service.js @@ -0,0 +1,104 @@ +const prisma = require('../../../config/prisma'); +const ApiError = require('../../../utils/ApiError'); +const auditLog = require('../../../utils/auditLog'); +const { getPagination } = require('../../../utils/pagination'); + +const createUom = async (payload, userId, requestId) => { + const exists = await prisma.uom.findFirst({ + where: { code: payload.code, deleted_at: null }, + }); + if (exists) throw new ApiError(409, 'UOM code already exists'); + + const data = { + ...payload, + code: payload.code.toUpperCase().trim(), + name: payload.name.trim(), + created_by: userId ? BigInt(userId) : null, + updated_by: userId ? BigInt(userId) : null, + }; + + const created = await prisma.uom.create({ data }); + await auditLog({ + tableName: 'uom', + recordId: created.id, + action: 'CREATE', + oldValue: null, + newValue: created, + userId, + requestId, + }); + return created; +}; + +const listUom = async (query) => { + const { page, limit, skip } = getPagination(query); + const where = { + deleted_at: null, + ...(query.is_active !== undefined ? { is_active: query.is_active } : {}), + ...(query.search + ? { + OR: [ + { code: { contains: query.search, mode: 'insensitive' } }, + { name: { contains: query.search, mode: 'insensitive' } }, + ], + } + : {}), + }; + + const [data, total] = await Promise.all([ + prisma.uom.findMany({ where, orderBy: { created_at: 'desc' }, skip, take: limit }), + prisma.uom.count({ where }), + ]); + + return { data, meta: { page, limit, total } }; +}; + +const getUomById = async (id) => { + const uom = await prisma.uom.findFirst({ where: { id: BigInt(id), deleted_at: null } }); + if (!uom) throw new ApiError(404, 'UOM not found'); + return uom; +}; + +const updateUom = async (id, payload, userId, requestId) => { + const existing = await getUomById(id); + const updated = await prisma.uom.update({ + where: { id: BigInt(id) }, + data: { + ...payload, + ...(payload.code ? { code: payload.code.toUpperCase().trim() } : {}), + ...(payload.name ? { name: payload.name.trim() } : {}), + updated_by: userId ? BigInt(userId) : null, + }, + }); + + await auditLog({ + tableName: 'uom', + recordId: id, + action: 'UPDATE', + oldValue: existing, + newValue: updated, + userId, + requestId, + }); + return updated; +}; + +const deleteUom = async (id, userId, requestId) => { + const existing = await getUomById(id); + await prisma.uom.update({ + where: { id: BigInt(id) }, + data: { deleted_at: new Date(), updated_by: userId ? BigInt(userId) : null }, + }); + + await auditLog({ + tableName: 'uom', + recordId: id, + action: 'DELETE', + oldValue: existing, + newValue: { deleted_at: new Date() }, + userId, + requestId, + }); +}; + +module.exports = { createUom, listUom, getUomById, updateUom, deleteUom }; diff --git a/src/modules/masters/uom/uom.validation.js b/src/modules/masters/uom/uom.validation.js new file mode 100644 index 0000000..8a04795 --- /dev/null +++ b/src/modules/masters/uom/uom.validation.js @@ -0,0 +1,22 @@ +const Joi = require('joi'); + +const createUomSchema = Joi.object({ + code: Joi.string().max(20).required(), + name: Joi.string().max(100).required(), + is_active: Joi.boolean().optional(), +}); + +const updateUomSchema = Joi.object({ + code: Joi.string().max(20).optional(), + name: Joi.string().max(100).optional(), + is_active: Joi.boolean().optional(), +}).min(1); + +const listUomQuerySchema = Joi.object({ + page: Joi.number().integer().min(1).default(1), + limit: Joi.number().integer().min(1).max(100).default(20), + search: Joi.string().allow('').optional(), + is_active: Joi.boolean().optional(), +}); + +module.exports = { createUomSchema, updateUomSchema, listUomQuerySchema }; diff --git a/src/modules/masters/warehouses/warehouses.controller.js b/src/modules/masters/warehouses/warehouses.controller.js new file mode 100644 index 0000000..71e2b4c --- /dev/null +++ b/src/modules/masters/warehouses/warehouses.controller.js @@ -0,0 +1,30 @@ +const asyncHandler = require('../../../utils/asyncHandler'); +const ApiResponse = require('../../../utils/ApiResponse'); +const service = require('./warehouses.service'); + +const create = asyncHandler(async (req, res) => { + const data = await service.createWarehouses(req.body, req.user?.id, req.id); + res.status(201).json(new ApiResponse(201, data, 'warehouses created successfully')); +}); + +const list = asyncHandler(async (req, res) => { + const result = await service.listWarehouses(req.query); + res.json(new ApiResponse(200, result.data, 'warehouses list fetched', result.meta)); +}); + +const getOne = asyncHandler(async (req, res) => { + const data = await service.getWarehousesById(req.params.id); + res.json(new ApiResponse(200, data, 'warehouses fetched')); +}); + +const update = asyncHandler(async (req, res) => { + const data = await service.updateWarehouses(req.params.id, req.body, req.user?.id, req.id); + res.json(new ApiResponse(200, data, 'warehouses updated successfully')); +}); + +const remove = asyncHandler(async (req, res) => { + await service.deleteWarehouses(req.params.id, req.user?.id, req.id); + res.json(new ApiResponse(200, null, 'warehouses deleted successfully')); +}); + +module.exports = { create, list, getOne, update, remove }; diff --git a/src/modules/masters/warehouses/warehouses.routes.js b/src/modules/masters/warehouses/warehouses.routes.js new file mode 100644 index 0000000..b9f1149 --- /dev/null +++ b/src/modules/masters/warehouses/warehouses.routes.js @@ -0,0 +1,17 @@ +const express = require('express'); +const authenticate = require('../../../middlewares/auth.middleware'); +const authorize = require('../../../middlewares/rbac.middleware'); +const validate = require('../../../middlewares/validate.middleware'); +const controller = require('./warehouses.controller'); +const { createSchema, updateSchema, listQuerySchema } = require('./warehouses.validation'); + +const router = express.Router(); + +router.use(authenticate); +router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list); +router.get('/:id', authorize('MASTERS', 'view'), controller.getOne); +router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create); +router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update); +router.delete('/:id', authorize('MASTERS', 'delete'), controller.remove); + +module.exports = router; diff --git a/src/modules/masters/warehouses/warehouses.service.js b/src/modules/masters/warehouses/warehouses.service.js new file mode 100644 index 0000000..07a175a --- /dev/null +++ b/src/modules/masters/warehouses/warehouses.service.js @@ -0,0 +1,62 @@ +const { buildMasterService } = require('../_shared/master.factory'); + +const config = { + modelName: 'warehouses', + tableName: 'warehouses', + uniqueField: 'code', + softDelete: true, + fields: [ + { + name: 'code', + type: 'string', + uppercase: true, + searchable: true, + }, + { + name: 'name', + type: 'string', + uppercase: false, + searchable: true, + }, + { + name: 'plant_id', + type: 'int', + uppercase: false, + searchable: false, + }, + { + name: 'location', + type: 'string', + uppercase: false, + searchable: false, + }, + { + name: 'is_active', + type: 'boolean', + uppercase: false, + searchable: false, + }, + { + name: 'created_by', + type: 'int', + uppercase: false, + searchable: false, + }, + { + name: 'updated_by', + type: 'int', + uppercase: false, + searchable: false, + }, + ], +}; + +const service = buildMasterService(config); + +module.exports = { + createWarehouses: service.createOne, + listWarehouses: service.list, + getWarehousesById: service.getOne, + updateWarehouses: service.updateOne, + deleteWarehouses: service.removeOne, +}; diff --git a/src/modules/masters/warehouses/warehouses.validation.js b/src/modules/masters/warehouses/warehouses.validation.js new file mode 100644 index 0000000..6f846d5 --- /dev/null +++ b/src/modules/masters/warehouses/warehouses.validation.js @@ -0,0 +1,20 @@ +const Joi = require('joi'); + +const createSchema = Joi.object({ + code: Joi.string().allow('').optional(), + name: Joi.string().allow('').optional(), + plant_id: Joi.number().integer().optional(), + location: Joi.string().allow('').optional(), + is_active: Joi.boolean().optional(), +}); + +const updateSchema = createSchema.min(1); + +const listQuerySchema = Joi.object({ + page: Joi.number().integer().min(1).default(1), + limit: Joi.number().integer().min(1).max(100).default(20), + search: Joi.string().allow('').optional(), + is_active: Joi.boolean().optional(), +}); + +module.exports = { createSchema, updateSchema, listQuerySchema }; diff --git a/src/modules/roles/roles.controller.js b/src/modules/roles/roles.controller.js new file mode 100644 index 0000000..058cce6 --- /dev/null +++ b/src/modules/roles/roles.controller.js @@ -0,0 +1,76 @@ +const asyncHandler = require('../../utils/asyncHandler'); +const ApiResponse = require('../../utils/ApiResponse'); +const service = require('./roles.service'); + +const create = asyncHandler(async (req, res) => { + const data = await service.createRole(req.body, req.user?.id, req.id); + res.status(201).json(new ApiResponse(201, data, 'Role created successfully')); +}); + +const list = asyncHandler(async (req, res) => { + const result = await service.listRoles(req.query); + res.json(new ApiResponse(200, result.data, 'Roles list fetched', result.meta)); +}); + +const getOne = asyncHandler(async (req, res) => { + const data = await service.getRoleById(req.params.id); + res.json(new ApiResponse(200, data, 'Role fetched')); +}); + +const update = asyncHandler(async (req, res) => { + const data = await service.updateRole(req.params.id, req.body, req.user?.id, req.id); + res.json(new ApiResponse(200, data, 'Role updated successfully')); +}); + +const remove = asyncHandler(async (req, res) => { + await service.deleteRole(req.params.id, req.user?.id, req.id); + res.json(new ApiResponse(200, null, 'Role deleted successfully')); +}); + +const listPermissions = asyncHandler(async (_req, res) => { + const data = await service.listPermissionCatalog(); + res.json(new ApiResponse(200, data, 'Permission catalog fetched')); +}); + +const assignPermissions = asyncHandler(async (req, res) => { + const data = await service.assignPermissions( + req.params.id, + req.body.permission_ids, + req.user?.id, + req.id + ); + res.json(new ApiResponse(200, data, 'Role permissions updated successfully')); +}); + +const listCards = asyncHandler(async (_req, res) => { + const data = await service.listRoleCards(); + res.json(new ApiResponse(200, data, 'Role cards fetched')); +}); + +const getPermissionMatrix = asyncHandler(async (req, res) => { + const data = await service.getPermissionMatrix(req.params.id); + res.json(new ApiResponse(200, data, 'Permission matrix fetched')); +}); + +const savePermissionMatrix = asyncHandler(async (req, res) => { + const data = await service.savePermissionMatrix( + req.params.id, + req.body.matrix, + req.user?.id, + req.id + ); + res.json(new ApiResponse(200, data, 'Permission matrix saved successfully')); +}); + +module.exports = { + create, + list, + getOne, + update, + remove, + listPermissions, + assignPermissions, + listCards, + getPermissionMatrix, + savePermissionMatrix, +}; diff --git a/src/modules/roles/roles.routes.js b/src/modules/roles/roles.routes.js new file mode 100644 index 0000000..2c6a25f --- /dev/null +++ b/src/modules/roles/roles.routes.js @@ -0,0 +1,43 @@ +const express = require('express'); +const authenticate = require('../../middlewares/auth.middleware'); +const authorize = require('../../middlewares/rbac.middleware'); +const validate = require('../../middlewares/validate.middleware'); +const controller = require('./roles.controller'); +const { + createRoleSchema, + updateRoleSchema, + assignPermissionsSchema, + permissionMatrixSchema, + listRolesQuerySchema, +} = require('./roles.validation'); + +const router = express.Router(); + +router.use(authenticate); +router.get('/permissions', authorize('ROLES', 'view'), controller.listPermissions); +router.get('/cards', authorize('ROLES', 'view'), controller.listCards); +router.get( + '/', + authorize('ROLES', 'view'), + validate(listRolesQuerySchema, 'query'), + controller.list +); +router.get('/:id', authorize('ROLES', 'view'), controller.getOne); +router.post('/', authorize('ROLES', 'create'), validate(createRoleSchema), controller.create); +router.put('/:id', authorize('ROLES', 'edit'), validate(updateRoleSchema), controller.update); +router.put( + '/:id/permissions', + authorize('ROLES', 'edit'), + validate(assignPermissionsSchema), + controller.assignPermissions +); +router.get('/:id/permission-matrix', authorize('ROLES', 'view'), controller.getPermissionMatrix); +router.put( + '/:id/permission-matrix', + authorize('ROLES', 'edit'), + validate(permissionMatrixSchema), + controller.savePermissionMatrix +); +router.delete('/:id', authorize('ROLES', 'delete'), controller.remove); + +module.exports = router; diff --git a/src/modules/roles/roles.service.js b/src/modules/roles/roles.service.js new file mode 100644 index 0000000..314b45a --- /dev/null +++ b/src/modules/roles/roles.service.js @@ -0,0 +1,355 @@ +const prisma = require('../../config/prisma'); +const ApiError = require('../../utils/ApiError'); +const auditLog = require('../../utils/auditLog'); +const { getPagination } = require('../../utils/pagination'); + +const PERMISSION_ACTIONS = ['view', 'create', 'edit', 'delete', 'approve', 'export']; + +const permissionSelect = { + permissions: { + select: { + id: true, + action: true, + is_active: true, + modules: { select: { id: true, code: true, name: true } }, + }, + }, +}; + +const rolePermissionsInclude = { role_permissions: { include: permissionSelect } }; + +const sanitizeRole = (role) => { + if (!role) return null; + const { role_permissions, ...rest } = role; + return { + ...rest, + permissions: (role_permissions || []).map((rp) => ({ + id: rp.permissions.id, + action: rp.permissions.action, + is_active: rp.permissions.is_active, + module: rp.permissions.modules, + })), + }; +}; + +const createRole = async (payload, userId, requestId) => { + const existing = await prisma.roles.findFirst({ + where: { name: payload.name, deleted_at: null }, + }); + if (existing) throw new ApiError(409, 'Role name already exists'); + + const created = await prisma.roles.create({ + data: { + name: payload.name.trim(), + description: payload.description ?? null, + is_active: payload.is_active ?? true, + created_by: userId ? BigInt(userId) : null, + updated_by: userId ? BigInt(userId) : null, + }, + include: rolePermissionsInclude, + }); + + await auditLog({ + tableName: 'roles', + recordId: created.id, + action: 'CREATE', + oldValue: null, + newValue: sanitizeRole(created), + userId, + requestId, + }); + + return sanitizeRole(created); +}; + +const listRoles = async (query) => { + const { page, limit, skip } = getPagination(query); + + const where = { + deleted_at: null, + ...(query.is_active !== undefined ? { is_active: query.is_active } : {}), + ...(query.search + ? { + OR: [ + { name: { contains: query.search, mode: 'insensitive' } }, + { description: { contains: query.search, mode: 'insensitive' } }, + ], + } + : {}), + }; + + const [rows, total] = await Promise.all([ + prisma.roles.findMany({ + where, + include: { + role_permissions: { include: permissionSelect }, + _count: { select: { users_users_role_idToroles: true } }, + }, + orderBy: { created_at: 'desc' }, + skip, + take: limit, + }), + prisma.roles.count({ where }), + ]); + + const data = rows.map((role) => ({ + ...sanitizeRole(role), + user_count: role._count.users_users_role_idToroles, + })); + + return { data, meta: { page, limit, total } }; +}; + +const getRoleById = async (id) => { + const role = await prisma.roles.findFirst({ + where: { id: BigInt(id), deleted_at: null }, + include: { + role_permissions: { include: permissionSelect }, + _count: { select: { users_users_role_idToroles: true } }, + }, + }); + if (!role) throw new ApiError(404, 'Role not found'); + return { ...sanitizeRole(role), user_count: role._count.users_users_role_idToroles }; +}; + +const updateRole = async (id, payload, userId, requestId) => { + const existing = await prisma.roles.findFirst({ + where: { id: BigInt(id), deleted_at: null }, + include: rolePermissionsInclude, + }); + if (!existing) throw new ApiError(404, 'Role not found'); + + if (payload.name) { + const duplicate = await prisma.roles.findFirst({ + where: { name: payload.name, deleted_at: null, id: { not: BigInt(id) } }, + }); + if (duplicate) throw new ApiError(409, 'Role name already exists'); + } + + const data = { + ...(payload.name !== undefined ? { name: payload.name.trim() } : {}), + ...(payload.description !== undefined ? { description: payload.description || null } : {}), + ...(payload.is_active !== undefined ? { is_active: payload.is_active } : {}), + updated_by: userId ? BigInt(userId) : null, + }; + + const updated = await prisma.roles.update({ + where: { id: BigInt(id) }, + data, + include: rolePermissionsInclude, + }); + + await auditLog({ + tableName: 'roles', + recordId: id, + action: 'UPDATE', + oldValue: sanitizeRole(existing), + newValue: sanitizeRole(updated), + userId, + requestId, + }); + + return sanitizeRole(updated); +}; + +const deleteRole = async (id, userId, requestId) => { + const existing = await prisma.roles.findFirst({ + where: { id: BigInt(id), deleted_at: null }, + include: rolePermissionsInclude, + }); + if (!existing) throw new ApiError(404, 'Role not found'); + + const assignedUsers = await prisma.users.count({ + where: { role_id: BigInt(id), deleted_at: null }, + }); + if (assignedUsers > 0) { + throw new ApiError(400, 'Cannot delete role assigned to active users'); + } + + await prisma.roles.update({ + where: { id: BigInt(id) }, + data: { deleted_at: new Date(), updated_by: userId ? BigInt(userId) : null, is_active: false }, + }); + + await auditLog({ + tableName: 'roles', + recordId: id, + action: 'DELETE', + oldValue: sanitizeRole(existing), + newValue: { deleted_at: new Date() }, + userId, + requestId, + }); +}; + +const listPermissionCatalog = async () => { + const modules = await prisma.modules.findMany({ + where: { is_active: true }, + orderBy: { sort_order: 'asc' }, + include: { + permissions: { + where: { is_active: true }, + orderBy: { action: 'asc' }, + select: { id: true, action: true, is_active: true }, + }, + }, + }); + + return modules.map((m) => ({ + id: m.id, + code: m.code, + name: m.name, + permissions: m.permissions, + })); +}; + +const assignPermissions = async (id, permissionIds, userId, requestId) => { + const role = await prisma.roles.findFirst({ where: { id: BigInt(id), deleted_at: null } }); + if (!role) throw new ApiError(404, 'Role not found'); + + const uniqueIds = [...new Set(permissionIds.map((pid) => BigInt(pid)))]; + + if (uniqueIds.length > 0) { + const permissions = await prisma.permissions.findMany({ + where: { id: { in: uniqueIds }, is_active: true }, + include: { modules: { select: { code: true } } }, + }); + + if (permissions.length !== uniqueIds.length) { + throw new ApiError(422, 'One or more permission_ids are invalid'); + } + } + + const before = await prisma.role_permissions.findMany({ + where: { role_id: BigInt(id) }, + include: permissionSelect, + }); + + await prisma.$transaction([ + prisma.role_permissions.deleteMany({ where: { role_id: BigInt(id) } }), + prisma.role_permissions.createMany({ + data: uniqueIds.map((permission_id) => ({ role_id: BigInt(id), permission_id })), + }), + prisma.roles.update({ + where: { id: BigInt(id) }, + data: { updated_by: userId ? BigInt(userId) : null }, + }), + ]); + + const updated = await prisma.roles.findFirst({ + where: { id: BigInt(id) }, + include: rolePermissionsInclude, + }); + + await auditLog({ + tableName: 'role_permissions', + recordId: id, + action: 'UPDATE', + oldValue: before.map((rp) => rp.permissions.id.toString()), + newValue: uniqueIds.map((pid) => pid.toString()), + userId, + requestId, + }); + + return sanitizeRole(updated); +}; + +const listRoleCards = async () => { + const rows = await prisma.roles.findMany({ + where: { deleted_at: null }, + include: { + role_permissions: { include: permissionSelect }, + _count: { select: { users_users_role_idToroles: true } }, + }, + orderBy: { created_at: 'asc' }, + }); + + return rows.map((role) => ({ + id: role.id, + name: role.name, + description: role.description, + is_active: role.is_active, + user_count: role._count.users_users_role_idToroles, + permission_count: role.role_permissions.length, + })); +}; + +const getPermissionMatrix = async (id) => { + const role = await prisma.roles.findFirst({ + where: { id: BigInt(id), deleted_at: null }, + include: { role_permissions: { include: permissionSelect } }, + }); + if (!role) throw new ApiError(404, 'Role not found'); + + const granted = new Set(role.role_permissions.map((rp) => rp.permissions.id.toString())); + + const modules = await prisma.modules.findMany({ + where: { is_active: true }, + orderBy: { sort_order: 'asc' }, + include: { + permissions: { where: { is_active: true }, orderBy: { action: 'asc' } }, + }, + }); + + const matrix = modules.map((mod) => { + const permissions = {}; + for (const action of PERMISSION_ACTIONS) { + const perm = mod.permissions.find((p) => p.action === action); + permissions[action] = { + permission_id: perm?.id ?? null, + granted: perm ? granted.has(perm.id.toString()) : false, + }; + } + return { + module_id: mod.id, + code: mod.code, + name: mod.name, + permissions, + }; + }); + + return { + role: { id: role.id, name: role.name }, + actions: PERMISSION_ACTIONS, + modules: matrix, + }; +}; + +const savePermissionMatrix = async (id, matrix, userId, requestId) => { + const role = await prisma.roles.findFirst({ where: { id: BigInt(id), deleted_at: null } }); + if (!role) throw new ApiError(404, 'Role not found'); + + const moduleIds = matrix.map((row) => BigInt(row.module_id)); + const catalog = await prisma.permissions.findMany({ + where: { is_active: true, modules: { id: { in: moduleIds }, is_active: true } }, + select: { id: true, module_id: true, action: true }, + }); + + const permissionIds = []; + for (const row of matrix) { + for (const action of PERMISSION_ACTIONS) { + if (!row.actions?.[action]) continue; + const perm = catalog.find( + (p) => p.module_id.toString() === String(row.module_id) && p.action === action + ); + if (!perm) + throw new ApiError(422, `Permission not found for module ${row.module_id}:${action}`); + permissionIds.push(perm.id); + } + } + + return assignPermissions(id, permissionIds, userId, requestId); +}; + +module.exports = { + createRole, + listRoles, + getRoleById, + updateRole, + deleteRole, + listPermissionCatalog, + assignPermissions, + listRoleCards, + getPermissionMatrix, + savePermissionMatrix, +}; diff --git a/src/modules/roles/roles.validation.js b/src/modules/roles/roles.validation.js new file mode 100644 index 0000000..1e8588b --- /dev/null +++ b/src/modules/roles/roles.validation.js @@ -0,0 +1,51 @@ +const Joi = require('joi'); + +const createRoleSchema = Joi.object({ + name: Joi.string().max(100).required(), + description: Joi.string().allow(null, '').optional(), + is_active: Joi.boolean().default(true), +}); + +const updateRoleSchema = Joi.object({ + name: Joi.string().max(100).optional(), + description: Joi.string().allow(null, '').optional(), + is_active: Joi.boolean().optional(), +}).min(1); + +const assignPermissionsSchema = Joi.object({ + permission_ids: Joi.array().items(Joi.number().integer().positive()).default([]), +}); + +const permissionMatrixSchema = Joi.object({ + matrix: Joi.array() + .items( + Joi.object({ + module_id: Joi.number().integer().positive().required(), + actions: Joi.object({ + view: Joi.boolean().required(), + create: Joi.boolean().required(), + edit: Joi.boolean().required(), + delete: Joi.boolean().required(), + approve: Joi.boolean().required(), + export: Joi.boolean().required(), + }).required(), + }) + ) + .min(1) + .required(), +}); + +const listRolesQuerySchema = Joi.object({ + page: Joi.number().integer().min(1).default(1), + limit: Joi.number().integer().min(1).max(100).default(20), + search: Joi.string().allow('').optional(), + is_active: Joi.boolean().optional(), +}); + +module.exports = { + createRoleSchema, + updateRoleSchema, + assignPermissionsSchema, + permissionMatrixSchema, + listRolesQuerySchema, +}; diff --git a/src/modules/users/users.controller.js b/src/modules/users/users.controller.js new file mode 100644 index 0000000..4748419 --- /dev/null +++ b/src/modules/users/users.controller.js @@ -0,0 +1,47 @@ +const asyncHandler = require('../../utils/asyncHandler'); +const ApiResponse = require('../../utils/ApiResponse'); +const service = require('./users.service'); + +const create = asyncHandler(async (req, res) => { + const data = await service.createUser(req.body, req.user?.id, req.id); + res.status(201).json(new ApiResponse(201, data, 'User created successfully')); +}); + +const list = asyncHandler(async (req, res) => { + const result = await service.listUsers(req.query); + res.json(new ApiResponse(200, result.data, 'Users list fetched', result.meta)); +}); + +const getOne = asyncHandler(async (req, res) => { + const data = await service.getUserById(req.params.id); + res.json(new ApiResponse(200, data, 'User fetched')); +}); + +const update = asyncHandler(async (req, res) => { + const data = await service.updateUser(req.params.id, req.body, req.user?.id, req.id); + res.json(new ApiResponse(200, data, 'User updated successfully')); +}); + +const remove = asyncHandler(async (req, res) => { + await service.deleteUser(req.params.id, req.user?.id, req.id); + res.json(new ApiResponse(200, null, 'User deleted successfully')); +}); + +const summary = asyncHandler(async (_req, res) => { + const data = await service.getScreenSummary(); + res.json(new ApiResponse(200, data, 'Users & roles summary fetched')); +}); + +const filters = asyncHandler(async (_req, res) => { + const data = await service.getFilterOptions(); + res.json(new ApiResponse(200, data, 'User filter options fetched')); +}); + +const exportCsv = asyncHandler(async (req, res) => { + const csv = await service.exportUsers(req.query); + res.setHeader('Content-Type', 'text/csv; charset=utf-8'); + res.setHeader('Content-Disposition', 'attachment; filename="users-export.csv"'); + res.send(csv); +}); + +module.exports = { create, list, getOne, update, remove, summary, filters, exportCsv }; diff --git a/src/modules/users/users.routes.js b/src/modules/users/users.routes.js new file mode 100644 index 0000000..b5829ba --- /dev/null +++ b/src/modules/users/users.routes.js @@ -0,0 +1,35 @@ +const express = require('express'); +const authenticate = require('../../middlewares/auth.middleware'); +const authorize = require('../../middlewares/rbac.middleware'); +const validate = require('../../middlewares/validate.middleware'); +const controller = require('./users.controller'); +const { + createUserSchema, + updateUserSchema, + listUsersQuerySchema, + exportUsersQuerySchema, +} = require('./users.validation'); + +const router = express.Router(); + +router.use(authenticate); +router.get('/summary', authorize('USERS', 'view'), controller.summary); +router.get('/filters', authorize('USERS', 'view'), controller.filters); +router.get( + '/export', + authorize('USERS', 'export'), + validate(exportUsersQuerySchema, 'query'), + controller.exportCsv +); +router.get( + '/', + authorize('USERS', 'view'), + validate(listUsersQuerySchema, 'query'), + controller.list +); +router.get('/:id', authorize('USERS', 'view'), controller.getOne); +router.post('/', authorize('USERS', 'create'), validate(createUserSchema), controller.create); +router.put('/:id', authorize('USERS', 'edit'), validate(updateUserSchema), controller.update); +router.delete('/:id', authorize('USERS', 'delete'), controller.remove); + +module.exports = router; diff --git a/src/modules/users/users.service.js b/src/modules/users/users.service.js new file mode 100644 index 0000000..67fa56c --- /dev/null +++ b/src/modules/users/users.service.js @@ -0,0 +1,363 @@ +const bcrypt = require('bcrypt'); +const prisma = require('../../config/prisma'); +const env = require('../../config/env'); +const ApiError = require('../../utils/ApiError'); +const auditLog = require('../../utils/auditLog'); +const { getPagination } = require('../../utils/pagination'); +const { encrypt, decrypt } = require('../../utils/encryption'); + +const userInclude = { + roles_users_role_idToroles: { select: { id: true, name: true } }, + departments: { select: { id: true, name: true } }, + designations: { select: { id: true, name: true } }, + plants_users_plant_idToplants: { select: { id: true, code: true, name: true } }, + users_users_reporting_toTousers: { select: { id: true, full_name: true, employee_code: true } }, +}; + +const sanitizeUser = (user) => { + if (!user) return null; + const { + roles_users_role_idToroles, + plants_users_plant_idToplants, + users_users_reporting_toTousers, + departments, + designations, + ...rest + } = user; + delete rest.password_hash; + + return { + ...rest, + mobile: rest.mobile ? decrypt(rest.mobile) : null, + role: roles_users_role_idToroles || null, + department: departments || null, + designation: designations || null, + plant: plants_users_plant_idToplants || null, + reporting_manager: users_users_reporting_toTousers || null, + }; +}; + +const buildUsersWhere = (query) => ({ + deleted_at: null, + ...(query.status ? { status: query.status } : {}), + ...(query.is_active !== undefined ? { is_active: query.is_active } : {}), + ...(query.role_id ? { role_id: BigInt(query.role_id) } : {}), + ...(query.department_id ? { department_id: BigInt(query.department_id) } : {}), + ...(query.search + ? { + OR: [ + { full_name: { contains: query.search, mode: 'insensitive' } }, + { email: { contains: query.search, mode: 'insensitive' } }, + { employee_code: { contains: query.search, mode: 'insensitive' } }, + ], + } + : {}), +}); + +const toInitials = (name) => { + if (!name) return ''; + return name + .trim() + .split(/\s+/) + .slice(0, 2) + .map((part) => part[0]?.toUpperCase() || '') + .join(''); +}; + +const toUserListItem = (user) => { + const row = sanitizeUser(user); + return { + id: row.id, + full_name: row.full_name, + email: row.email, + initials: toInitials(row.full_name), + employee_code: row.employee_code, + role: row.role, + department: row.department, + plant: row.plant, + last_login_at: row.last_login_at, + status: row.status, + is_active: row.is_active, + }; +}; + +const assertFk = async (payload) => { + if (payload.role_id) { + const role = await prisma.roles.findFirst({ + where: { id: BigInt(payload.role_id), deleted_at: null, is_active: true }, + }); + if (!role) throw new ApiError(422, 'Invalid role_id'); + } + if (payload.department_id) { + const row = await prisma.departments.findFirst({ + where: { id: BigInt(payload.department_id), deleted_at: null }, + }); + if (!row) throw new ApiError(422, 'Invalid department_id'); + } + if (payload.designation_id) { + const row = await prisma.designations.findFirst({ + where: { id: BigInt(payload.designation_id), deleted_at: null }, + }); + if (!row) throw new ApiError(422, 'Invalid designation_id'); + } + if (payload.plant_id) { + const row = await prisma.plants.findFirst({ + where: { id: BigInt(payload.plant_id), deleted_at: null }, + }); + if (!row) throw new ApiError(422, 'Invalid plant_id'); + } + if (payload.reporting_to) { + const row = await prisma.users.findFirst({ + where: { id: BigInt(payload.reporting_to), deleted_at: null }, + }); + if (!row) throw new ApiError(422, 'Invalid reporting_to'); + } +}; + +const buildUserData = async (payload, { hashPassword = false } = {}) => { + const data = { ...payload }; + + if (hashPassword && data.password) { + data.password_hash = await bcrypt.hash(data.password, env.BCRYPT_SALT_ROUNDS); + delete data.password; + } else { + delete data.password; + } + + if (Object.prototype.hasOwnProperty.call(data, 'mobile')) { + data.mobile = data.mobile ? encrypt(data.mobile) : null; + } + + for (const key of ['role_id', 'department_id', 'designation_id', 'plant_id', 'reporting_to']) { + if (data[key] !== undefined && data[key] !== null) data[key] = BigInt(data[key]); + } + + return data; +}; + +const createUser = async (payload, userId, requestId) => { + await assertFk(payload); + + const existing = await prisma.users.findFirst({ + where: { + deleted_at: null, + OR: [{ email: payload.email }, { employee_code: payload.employee_code }], + }, + }); + if (existing) throw new ApiError(409, 'User with this email or employee code already exists'); + + const data = await buildUserData(payload, { hashPassword: true }); + data.created_by = userId ? BigInt(userId) : null; + data.updated_by = userId ? BigInt(userId) : null; + + const created = await prisma.users.create({ data, include: userInclude }); + + await auditLog({ + tableName: 'users', + recordId: created.id, + action: 'CREATE', + oldValue: null, + newValue: sanitizeUser(created), + userId, + requestId, + }); + + return sanitizeUser(created); +}; + +const listUsers = async (query) => { + const { page, limit, skip } = getPagination(query); + const where = buildUsersWhere(query); + + const [rows, total] = await Promise.all([ + prisma.users.findMany({ + where, + include: userInclude, + orderBy: { created_at: 'desc' }, + skip, + take: limit, + }), + prisma.users.count({ where }), + ]); + + return { data: rows.map(toUserListItem), meta: { page, limit, total } }; +}; + +const getScreenSummary = async () => { + const baseWhere = { deleted_at: null }; + + const [total, active, inactive, locked, rolesTotal] = await Promise.all([ + prisma.users.count({ where: baseWhere }), + prisma.users.count({ where: { ...baseWhere, status: 'active' } }), + prisma.users.count({ where: { ...baseWhere, status: 'inactive' } }), + prisma.users.count({ where: { ...baseWhere, status: 'locked' } }), + prisma.roles.count({ where: { deleted_at: null } }), + ]); + + return { + users: { total, active, inactive, locked }, + roles: { total: rolesTotal }, + tabs: { users: total, roles: rolesTotal }, + }; +}; + +const getFilterOptions = async () => { + const [roles, departments] = await Promise.all([ + prisma.roles.findMany({ + where: { deleted_at: null, is_active: true }, + select: { id: true, name: true }, + orderBy: { name: 'asc' }, + }), + prisma.departments.findMany({ + where: { deleted_at: null, is_active: true }, + select: { id: true, name: true }, + orderBy: { name: 'asc' }, + }), + ]); + + return { + roles, + departments, + statuses: [ + { value: 'active', label: 'Active' }, + { value: 'inactive', label: 'Inactive' }, + { value: 'locked', label: 'Locked' }, + ], + }; +}; + +const escapeCsv = (value) => { + const text = String(value ?? ''); + if (/[",\n]/.test(text)) return `"${text.replace(/"/g, '""')}"`; + return text; +}; + +const exportUsers = async (query) => { + const where = buildUsersWhere(query); + const rows = await prisma.users.findMany({ + where, + include: userInclude, + orderBy: { full_name: 'asc' }, + }); + + const header = [ + 'Full Name', + 'Email', + 'Employee Code', + 'Role', + 'Department', + 'Plant', + 'Last Login', + 'Status', + ]; + + const lines = rows.map((user) => { + const item = toUserListItem(user); + return [ + item.full_name, + item.email, + item.employee_code, + item.role?.name || '', + item.department?.name || '', + item.plant?.name || '', + item.last_login_at ? new Date(item.last_login_at).toISOString() : '', + item.status, + ] + .map(escapeCsv) + .join(','); + }); + + return [header.join(','), ...lines].join('\n'); +}; + +const getUserById = async (id) => { + const user = await prisma.users.findFirst({ + where: { id: BigInt(id), deleted_at: null }, + include: userInclude, + }); + if (!user) throw new ApiError(404, 'User not found'); + return sanitizeUser(user); +}; + +const updateUser = async (id, payload, userId, requestId) => { + const existing = await prisma.users.findFirst({ + where: { id: BigInt(id), deleted_at: null }, + include: userInclude, + }); + if (!existing) throw new ApiError(404, 'User not found'); + + await assertFk(payload); + + if (payload.email || payload.employee_code) { + const duplicate = await prisma.users.findFirst({ + where: { + deleted_at: null, + id: { not: BigInt(id) }, + OR: [ + ...(payload.email ? [{ email: payload.email }] : []), + ...(payload.employee_code ? [{ employee_code: payload.employee_code }] : []), + ], + }, + }); + if (duplicate) throw new ApiError(409, 'User with this email or employee code already exists'); + } + + const data = await buildUserData(payload, { hashPassword: Boolean(payload.password) }); + data.updated_by = userId ? BigInt(userId) : null; + + const updated = await prisma.users.update({ + where: { id: BigInt(id) }, + data, + include: userInclude, + }); + + await auditLog({ + tableName: 'users', + recordId: id, + action: 'UPDATE', + oldValue: sanitizeUser(existing), + newValue: sanitizeUser(updated), + userId, + requestId, + }); + + return sanitizeUser(updated); +}; + +const deleteUser = async (id, userId, requestId) => { + const existing = await prisma.users.findFirst({ + where: { id: BigInt(id), deleted_at: null }, + include: userInclude, + }); + if (!existing) throw new ApiError(404, 'User not found'); + + if (userId && String(existing.id) === String(userId)) { + throw new ApiError(400, 'You cannot delete your own account'); + } + + await prisma.users.update({ + where: { id: BigInt(id) }, + data: { deleted_at: new Date(), updated_by: userId ? BigInt(userId) : null, is_active: false }, + }); + + await auditLog({ + tableName: 'users', + recordId: id, + action: 'DELETE', + oldValue: sanitizeUser(existing), + newValue: { deleted_at: new Date() }, + userId, + requestId, + }); +}; + +module.exports = { + createUser, + listUsers, + getUserById, + updateUser, + deleteUser, + getScreenSummary, + getFilterOptions, + exportUsers, +}; diff --git a/src/modules/users/users.validation.js b/src/modules/users/users.validation.js new file mode 100644 index 0000000..1ea67d1 --- /dev/null +++ b/src/modules/users/users.validation.js @@ -0,0 +1,53 @@ +const Joi = require('joi'); + +const createUserSchema = Joi.object({ + employee_code: Joi.string().max(30).required(), + full_name: Joi.string().max(200).required(), + email: Joi.string().email().max(200).required(), + password: Joi.string().min(8).required(), + mobile: Joi.string().max(20).allow(null, '').optional(), + role_id: Joi.number().integer().positive().required(), + department_id: Joi.number().integer().positive().allow(null).optional(), + designation_id: Joi.number().integer().positive().allow(null).optional(), + plant_id: Joi.number().integer().positive().allow(null).optional(), + reporting_to: Joi.number().integer().positive().allow(null).optional(), + status: Joi.string().valid('active', 'inactive', 'locked').default('active'), + is_active: Joi.boolean().default(true), +}); + +const updateUserSchema = Joi.object({ + employee_code: Joi.string().max(30).optional(), + full_name: Joi.string().max(200).optional(), + email: Joi.string().email().max(200).optional(), + password: Joi.string().min(8).optional(), + mobile: Joi.string().max(20).allow(null, '').optional(), + role_id: Joi.number().integer().positive().optional(), + department_id: Joi.number().integer().positive().allow(null).optional(), + designation_id: Joi.number().integer().positive().allow(null).optional(), + plant_id: Joi.number().integer().positive().allow(null).optional(), + reporting_to: Joi.number().integer().positive().allow(null).optional(), + status: Joi.string().valid('active', 'inactive', 'locked').optional(), + is_active: Joi.boolean().optional(), +}).min(1); + +const listUsersQuerySchema = Joi.object({ + page: Joi.number().integer().min(1).default(1), + limit: Joi.number().integer().min(1).max(100).default(20), + search: Joi.string().allow('').optional(), + status: Joi.string().valid('active', 'inactive', 'locked').optional(), + is_active: Joi.boolean().optional(), + role_id: Joi.number().integer().positive().optional(), + department_id: Joi.number().integer().positive().optional(), +}); + +const exportUsersQuerySchema = listUsersQuerySchema.keys({ + page: Joi.strip(), + limit: Joi.strip(), +}); + +module.exports = { + createUserSchema, + updateUserSchema, + listUsersQuerySchema, + exportUsersQuerySchema, +}; diff --git a/src/routes/v1/index.js b/src/routes/v1/index.js new file mode 100644 index 0000000..648dcee --- /dev/null +++ b/src/routes/v1/index.js @@ -0,0 +1,14 @@ +const express = require('express'); + +const router = express.Router(); + +router.use('/auth', require('../../modules/auth/auth.routes')); +router.use('/users', require('../../modules/users/users.routes')); +router.use('/roles', require('../../modules/roles/roles.routes')); +router.use('/masters', require('../../modules/masters')); + +router.get('/healthz', (req, res) => { + res.json({ success: true, message: 'API v1 healthy' }); +}); + +module.exports = router; diff --git a/src/server.js b/src/server.js new file mode 100644 index 0000000..160edb2 --- /dev/null +++ b/src/server.js @@ -0,0 +1,46 @@ +const app = require('./app'); +const env = require('./config/env'); +const logger = require('./config/logger'); +const prisma = require('./config/prisma'); + +BigInt.prototype.toJSON = function toJSON() { + return this.toString(); +}; + +const server = app.listen(env.PORT, () => { + logger.info(`Server running on port ${env.PORT} [${env.NODE_ENV}]`); +}); + +server.on('error', (err) => { + if (err.code === 'EADDRINUSE') { + const message = `Port ${env.PORT} is already in use. Stop the other process or change PORT in .env`; + logger.error(message); + console.error(message); + process.exit(1); + } + + logger.error('Server failed to start', { error: err.message, stack: err.stack }); + console.error(err); + process.exit(1); +}); + +const shutdown = async (signal) => { + logger.info(`${signal} received - shutting down gracefully`); + server.close(async () => { + await prisma.$disconnect(); + logger.info('HTTP server closed, Prisma disconnected'); + process.exit(0); + }); +}; + +process.on('SIGTERM', () => shutdown('SIGTERM')); +process.on('SIGINT', () => shutdown('SIGINT')); + +process.on('unhandledRejection', (reason) => { + logger.error('Unhandled promise rejection', { reason: reason?.message || reason }); +}); + +process.on('uncaughtException', (err) => { + logger.error('Uncaught exception - exiting', { error: err.message, stack: err.stack }); + process.exit(1); +}); diff --git a/src/utils/ApiError.js b/src/utils/ApiError.js new file mode 100644 index 0000000..ab926ff --- /dev/null +++ b/src/utils/ApiError.js @@ -0,0 +1,11 @@ +class ApiError extends Error { + constructor(statusCode, message, errors = [], isOperational = true) { + super(message); + this.statusCode = statusCode; + this.errors = errors; + this.isOperational = isOperational; + Error.captureStackTrace(this, this.constructor); + } +} + +module.exports = ApiError; diff --git a/src/utils/ApiResponse.js b/src/utils/ApiResponse.js new file mode 100644 index 0000000..5500c44 --- /dev/null +++ b/src/utils/ApiResponse.js @@ -0,0 +1,10 @@ +class ApiResponse { + constructor(statusCode, data = null, message = 'Success', meta = null) { + this.success = statusCode < 400; + this.message = message; + if (data !== null) this.data = data; + if (meta !== null) this.meta = meta; + } +} + +module.exports = ApiResponse; diff --git a/src/utils/asyncHandler.js b/src/utils/asyncHandler.js new file mode 100644 index 0000000..ee7e63f --- /dev/null +++ b/src/utils/asyncHandler.js @@ -0,0 +1 @@ +module.exports = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next); diff --git a/src/utils/auditLog.js b/src/utils/auditLog.js new file mode 100644 index 0000000..699d759 --- /dev/null +++ b/src/utils/auditLog.js @@ -0,0 +1,15 @@ +const prisma = require('../config/prisma'); + +module.exports = async ({ tableName, recordId, action, oldValue, newValue, userId, requestId }) => { + await prisma.audit_logs.create({ + data: { + table_name: tableName, + record_id: BigInt(recordId), + action, + old_value: oldValue ?? undefined, + new_value: newValue ?? undefined, + performed_by: userId ? BigInt(userId) : null, + request_id: requestId, + }, + }); +}; diff --git a/src/utils/encryption.js b/src/utils/encryption.js new file mode 100644 index 0000000..1cd99fe --- /dev/null +++ b/src/utils/encryption.js @@ -0,0 +1,35 @@ +const crypto = require('crypto'); +const env = require('../config/env'); + +const ALGORITHM = 'aes-256-gcm'; +const KEY = Buffer.from(env.ENCRYPTION_KEY, 'hex'); + +const encrypt = (plainText) => { + if (plainText === null || plainText === undefined || plainText === '') return null; + const iv = crypto.randomBytes(12); + const cipher = crypto.createCipheriv(ALGORITHM, KEY, iv); + const encrypted = Buffer.concat([cipher.update(String(plainText), 'utf8'), cipher.final()]); + const authTag = cipher.getAuthTag(); + return Buffer.concat([iv, authTag, encrypted]).toString('base64'); +}; + +const decrypt = (payload) => { + if (!payload) return null; + const buffer = Buffer.from(payload, 'base64'); + const iv = buffer.subarray(0, 12); + const authTag = buffer.subarray(12, 28); + const encrypted = buffer.subarray(28); + const decipher = crypto.createDecipheriv(ALGORITHM, KEY, iv); + decipher.setAuthTag(authTag); + return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf8'); +}; + +const blindIndex = (value) => { + if (!value) return null; + return crypto + .createHmac('sha256', env.ENCRYPTION_HMAC_KEY) + .update(String(value).trim().toLowerCase()) + .digest('hex'); +}; + +module.exports = { encrypt, decrypt, blindIndex }; diff --git a/src/utils/generateCode.js b/src/utils/generateCode.js new file mode 100644 index 0000000..4291eb6 --- /dev/null +++ b/src/utils/generateCode.js @@ -0,0 +1,19 @@ +const prisma = require('../config/prisma'); +const ApiError = require('./ApiError'); + +const nextDocumentNumber = async (seriesCode) => + prisma.$transaction(async (tx) => { + const series = await tx.document_series.findUnique({ where: { code: seriesCode } }); + if (!series) throw new ApiError(400, `Document series not configured: ${seriesCode}`); + + const nextSeq = series.current_number + 1; + await tx.document_series.update({ + where: { code: seriesCode }, + data: { current_number: nextSeq }, + }); + + const padded = String(nextSeq).padStart(series.padding || 5, '0'); + return `${series.prefix}${padded}`; + }); + +module.exports = { nextDocumentNumber }; diff --git a/src/utils/pagination.js b/src/utils/pagination.js new file mode 100644 index 0000000..93cfeaf --- /dev/null +++ b/src/utils/pagination.js @@ -0,0 +1,8 @@ +const getPagination = (query) => { + const page = Math.max(Number(query.page) || 1, 1); + const limit = Math.min(Math.max(Number(query.limit) || 20, 1), 100); + const skip = (page - 1) * limit; + return { page, limit, skip }; +}; + +module.exports = { getPagination };