diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 29d6828..0000000 --- a/.dockerignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -npm-debug.log - diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index 2294764..b33af34 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,89 @@ -/node_modules -/dist -/.env -/generated/prisma +# Dependencies +node_modules/ + +# TypeScript build output +dist/ + +# Environment variables +.env +.env.local +.env.development +.env.test +.env.production + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db +/src/generated/prisma diff --git a/CATAGORY_API.md b/CATAGORY_API.md deleted file mode 100644 index 3cf41e9..0000000 --- a/CATAGORY_API.md +++ /dev/null @@ -1,577 +0,0 @@ -# Categories API Documentation - -## Overview -This API provides endpoints for managing categories in the application. Categories are used to organize services hierarchically and can have parent-child relationships for better organization. - -## Base URL -``` -http://localhost:3000/api/categories -``` - -## Endpoints - -### 1. Create a Category -**POST** `/api/categories` - -Creates a new category. ~~Requires authentication~~ **Public for testing**. - -#### Request Body: -```json -{ - "name": "Web Development", - "slug": "web-development", - "description": "Professional web development and programming services", - "parentId": "cuid_parent_category_id" -} -``` - -#### Response (201): -```json -{ - "success": true, - "message": "Category created successfully", - "data": { - "id": "cuid_category_id", - "name": "Web Development", - "slug": "web-development", - "description": "Professional web development and programming services", - "parentId": "cuid_parent_category_id", - "parent": { - "id": "cuid_parent_category_id", - "name": "Technology", - "slug": "technology" - }, - "children": [], - "_count": { - "services": 0 - } - } -} -``` - -### 2. Get All Categories -**GET** `/api/categories` - -Retrieves all categories with optional filtering. - -#### Query Parameters: -- `parentId` (optional): Filter by parent category ID (use `null` for root categories) -- `includeChildren` (optional): Include children categories (`true` or `false`, default: `true`) -- `includeParent` (optional): Include parent category info (`true` or `false`, default: `true`) -- `includeServices` (optional): Include services count (`true` or `false`, default: `false`) - -#### Example: -``` -GET /api/categories?parentId=cuid_parent_id&includeChildren=true&includeServices=true -``` - -#### Response (200): -```json -{ - "success": true, - "message": "Categories retrieved successfully", - "data": [ - { - "id": "cuid_category_id", - "name": "Web Development", - "slug": "web-development", - "description": "Professional web development services", - "parentId": "cuid_parent_category_id", - "parent": { - "id": "cuid_parent_category_id", - "name": "Technology", - "slug": "technology" - }, - "children": [ - { - "id": "cuid_child_category_id", - "name": "Frontend Development", - "slug": "frontend-development", - "description": "Frontend web development services" - } - ], - "_count": { - "services": 5 - } - } - ] -} -``` - -### 3. Get Root Categories -**GET** `/api/categories/roots` - -Retrieves all root categories (categories with no parent). - -#### Query Parameters: -- `includeChildren` (optional): Include children categories (`true` or `false`, default: `true`) - -#### Example: -``` -GET /api/categories/roots?includeChildren=true -``` - -#### Response (200): -```json -{ - "success": true, - "message": "Root categories retrieved successfully", - "data": [ - { - "id": "cuid_category_id", - "name": "Technology", - "slug": "technology", - "description": "Technology related services", - "parentId": null, - "children": [ - { - "id": "cuid_child_category_id", - "name": "Web Development", - "slug": "web-development", - "description": "Web development services" - } - ], - "_count": { - "services": 15 - } - } - ] -} -``` - -### 4. Search Categories -**GET** `/api/categories/search` - -Search categories by name or description. - -#### Query Parameters: -- `q` (required): Search term -- `includeChildren` (optional): Include children categories (`true` or `false`, default: `true`) -- `includeParent` (optional): Include parent category info (`true` or `false`, default: `true`) - -#### Example: -``` -GET /api/categories/search?q=web&includeChildren=true -``` - -#### Response (200): -```json -{ - "success": true, - "message": "Search completed successfully", - "data": [ - { - "id": "cuid_category_id", - "name": "Web Development", - "slug": "web-development", - "description": "Professional web development services", - "parentId": "cuid_parent_category_id", - "parent": { - "id": "cuid_parent_category_id", - "name": "Technology", - "slug": "technology" - }, - "children": [], - "_count": { - "services": 8 - } - } - ], - "searchTerm": "web" -} -``` - -### 5. Get Category by ID -**GET** `/api/categories/id/:id` - -Retrieves a specific category by its ID. - -#### Query Parameters: -- `includeChildren` (optional): Include children categories (`true` or `false`, default: `true`) -- `includeParent` (optional): Include parent category info (`true` or `false`, default: `true`) -- `includeServices` (optional): Include associated services (`true` or `false`, default: `false`) - -#### Example: -``` -GET /api/categories/id/cuid_category_id?includeServices=true -``` - -#### Response (200): -```json -{ - "success": true, - "message": "Category retrieved successfully", - "data": { - "id": "cuid_category_id", - "name": "Web Development", - "slug": "web-development", - "description": "Professional web development services", - "parentId": "cuid_parent_category_id", - "parent": { - "id": "cuid_parent_category_id", - "name": "Technology", - "slug": "technology", - "description": "Technology related services" - }, - "children": [ - { - "id": "cuid_child_category_id", - "name": "Frontend Development", - "slug": "frontend-development", - "description": "Frontend web development services" - } - ], - "services": [ - { - "id": "cuid_service_id", - "title": "React Development Service", - "description": "Professional React development", - "price": 99.99, - "currency": "USD", - "isActive": true - } - ], - "_count": { - "services": 8 - } - } -} -``` - -### 6. Get Category by Slug -**GET** `/api/categories/slug/:slug` - -Retrieves a specific category by its slug. - -#### Query Parameters: -- `includeChildren` (optional): Include children categories (`true` or `false`, default: `true`) -- `includeParent` (optional): Include parent category info (`true` or `false`, default: `true`) -- `includeServices` (optional): Include associated services (`true` or `false`, default: `false`) - -#### Example: -``` -GET /api/categories/slug/web-development?includeChildren=true -``` - -#### Response (200): -```json -{ - "success": true, - "message": "Category retrieved successfully", - "data": { - "id": "cuid_category_id", - "name": "Web Development", - "slug": "web-development", - "description": "Professional web development services", - "parentId": "cuid_parent_category_id", - "parent": { - "id": "cuid_parent_category_id", - "name": "Technology", - "slug": "technology", - "description": "Technology related services" - }, - "children": [ - { - "id": "cuid_child_category_id", - "name": "Frontend Development", - "slug": "frontend-development", - "description": "Frontend web development services" - } - ], - "_count": { - "services": 8 - } - } -} -``` - -### 7. Get Category Hierarchy -**GET** `/api/categories/:id/hierarchy` - -Gets the full hierarchy tree starting from a specific category (up to 3 levels up and down). - -#### Example: -``` -GET /api/categories/cuid_category_id/hierarchy -``` - -#### Response (200): -```json -{ - "success": true, - "message": "Category hierarchy retrieved successfully", - "data": { - "id": "cuid_category_id", - "name": "Web Development", - "slug": "web-development", - "description": "Professional web development services", - "parentId": "cuid_parent_category_id", - "parent": { - "id": "cuid_parent_category_id", - "name": "Technology", - "slug": "technology", - "parent": { - "id": "cuid_grandparent_id", - "name": "Services", - "slug": "services", - "parent": null - } - }, - "children": [ - { - "id": "cuid_child_category_id", - "name": "Frontend Development", - "slug": "frontend-development", - "children": [ - { - "id": "cuid_grandchild_id", - "name": "React Development", - "slug": "react-development", - "children": [] - } - ] - } - ] - } -} -``` - -### 8. Update Category -**PUT** `/api/categories/:id` - -Updates an existing category. Requires authentication. - -#### Request Body (all fields optional): -```json -{ - "name": "Updated Web Development", - "slug": "updated-web-development", - "description": "Updated description for web development services", - "parentId": "new_parent_category_id" -} -``` - -#### Response (200): -```json -{ - "success": true, - "message": "Category updated successfully", - "data": { - "id": "cuid_category_id", - "name": "Updated Web Development", - "slug": "updated-web-development", - "description": "Updated description for web development services", - "parentId": "new_parent_category_id", - "parent": { - "id": "new_parent_category_id", - "name": "Technology", - "slug": "technology" - }, - "children": [], - "_count": { - "services": 8 - } - } -} -``` - -### 9. Delete Category -**DELETE** `/api/categories/:id` - -Deletes a category. Requires authentication. - -#### Query Parameters: -- `force` (optional): Force delete even if category has children or services (`true` or `false`, default: `false`) - -#### Example: -``` -DELETE /api/categories/cuid_category_id?force=true -``` - -#### Response (200): -```json -{ - "success": true, - "message": "Category deleted successfully", - "data": { - "id": "cuid_category_id", - "name": "Web Development", - "slug": "web-development", - "description": "Professional web development services", - "parentId": "cuid_parent_category_id" - } -} -``` - -## Error Responses - -### Validation Error (400): -```json -{ - "success": false, - "message": "Validation failed", - "errors": [ - { - "field": "slug", - "message": "Slug is required" - } - ] -} -``` - -### Duplicate Slug Error (400): -```json -{ - "success": false, - "message": "Category with this slug already exists" -} -``` - -### Not Found Error (404): -```json -{ - "success": false, - "message": "Category not found" -} -``` - -### Circular Reference Error (400): -```json -{ - "success": false, - "message": "Cannot create circular reference in category hierarchy" -} -``` - -### Delete Restriction Error (400): -```json -{ - "success": false, - "message": "Cannot delete category with child categories. Use force option or delete children first." -} -``` - -### Server Error (500): -```json -{ - "success": false, - "message": "Failed to create category: Parent category not found" -} -``` - -## Field Validations - -### Required Fields (for creation): -- `slug`: Must be unique, 2-100 characters, lowercase letters, numbers, and hyphens only - -### Optional Fields: -- `name`: 2-100 characters -- `description`: 10-500 characters -- `parentId`: Must be a valid category ID or null - -### Slug Format Rules: -- Must be lowercase -- Can contain letters (a-z), numbers (0-9), and hyphens (-) -- Cannot start or end with a hyphen -- Must be unique across all categories - -Examples of valid slugs: -- `web-development` -- `mobile-apps` -- `digital-marketing` -- `graphic-design` - -## Hierarchy Rules - -1. **No Circular References**: A category cannot be its own ancestor -2. **Parent Validation**: Parent category must exist before assigning children -3. **Depth Limit**: Recommended maximum depth of 4-5 levels for performance -4. **Deletion Rules**: - - Cannot delete categories with children unless `force=true` - - Cannot delete categories with associated services unless `force=true` - - When force deleting, children become root categories - -## Testing with curl - -### Create a root category: -```bash -curl -X POST http://localhost:3000/api/categories \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer your_jwt_token" \ - -d '{ - "name": "Technology", - "slug": "technology", - "description": "Technology related services" - }' -``` - -### Create a subcategory: -```bash -curl -X POST http://localhost:3000/api/categories \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer your_jwt_token" \ - -d '{ - "name": "Web Development", - "slug": "web-development", - "description": "Web development services", - "parentId": "your_parent_category_id" - }' -``` - -### Get all root categories: -```bash -curl http://localhost:3000/api/categories/roots -``` - -### Search categories: -```bash -curl "http://localhost:3000/api/categories/search?q=web" -``` - -### Get category by slug: -```bash -curl http://localhost:3000/api/categories/slug/web-development -``` - -### Update a category: -```bash -curl -X PUT http://localhost:3000/api/categories/your_category_id \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer your_jwt_token" \ - -d '{ - "name": "Updated Category Name", - "description": "Updated description" - }' -``` - -### Delete a category: -```bash -curl -X DELETE http://localhost:3000/api/categories/your_category_id \ - -H "Authorization: Bearer your_jwt_token" -``` - -## Common Use Cases - -### 1. Building a Category Tree for UI: -```bash -# Get all root categories with their children -curl http://localhost:3000/api/categories/roots?includeChildren=true -``` - -### 2. Category Navigation Breadcrumbs: -```bash -# Get full hierarchy for a category -curl http://localhost:3000/api/categories/your_category_id/hierarchy -``` - -### 3. Service Filtering by Category: -```bash -# Get category with service count -curl "http://localhost:3000/api/categories/id/your_category_id?includeServices=false" -``` - -### 4. Admin Category Management: -```bash -# Get all categories with full details -curl "http://localhost:3000/api/categories?includeChildren=true&includeServices=true" -``` diff --git a/EMBEDDING_FIX_GUIDE.md b/EMBEDDING_FIX_GUIDE.md new file mode 100755 index 0000000..3b583a2 --- /dev/null +++ b/EMBEDDING_FIX_GUIDE.md @@ -0,0 +1,188 @@ +# Service Embeddings Fix - Implementation Guide + +## Problem Solved + +The issue was that when creating services in the database, the embedding fields (`titleEmbedding`, `descriptionEmbedding`, `tagsEmbedding`, `combinedEmbedding`) were remaining null. This prevented the semantic search functionality from working properly. + +## Changes Made + +### 1. Fixed Service Creation Flow + +**File: `src/services/services.service.ts`** + +- Added import for `embeddingService` +- Modified `createService()` function to automatically generate and store embeddings after service creation +- Added error handling to ensure service creation doesn't fail if embedding generation fails + +### 2. Fixed Service Update Flow + +**File: `src/services/services.service.ts`** + +- Modified `updateService()` function to detect when content changes (title, description, or tags) +- Automatically regenerates embeddings when content affecting search relevance is updated +- Added error handling to ensure service updates don't fail if embedding regeneration fails + +### 3. Created Migration Scripts + +**Files: `scripts/generate-missing-embeddings.js` and `scripts/generate-missing-embeddings.ts`** + +- Comprehensive migration script to generate embeddings for all existing services that don't have them +- Includes progress tracking, error handling, and rate limiting for API quotas +- Supports both JavaScript and TypeScript execution +- Added prerequisites checking to ensure the system is ready + +### 4. Updated Package.json + +Added convenient npm scripts: +- `npm run generate-embeddings` - Run JavaScript version (requires build first) +- `npm run generate-embeddings:ts` - Run TypeScript version directly + +## How to Use + +### For New Services +No action needed! When you create a new service through the API, embeddings will be automatically generated and stored. + +### For Existing Services Without Embeddings + +#### Option 1: Run Migration Script (Recommended) +```bash +# Navigate to backend directory +cd backend + +# Option A: Run TypeScript version directly +npm run generate-embeddings:ts + +# Option B: Build and run JavaScript version +npm run generate-embeddings +``` + +#### Option 2: Use API Endpoints +```bash +# Update embeddings for a specific service +POST /api/services/:serviceId/embeddings + +# Update embeddings for all services (batch) +POST /api/services/embeddings/batch +``` + +## Migration Script Features + +### ✅ Safety Features +- **Prerequisites Check**: Verifies database connection, pgvector extension, and embedding service +- **Graceful Handling**: Continues processing even if individual services fail +- **Rate Limiting**: Respects API quotas with 5-second delays between requests +- **Progress Tracking**: Shows detailed progress with estimated completion time +- **Graceful Shutdown**: Handles Ctrl+C interruption safely + +### âš ī¸ Important Notes +- **API Quotas**: Uses Gemini API for embeddings - free tier has daily limits +- **Time Required**: Processes 5 services per batch with 5-second delays (rate limiting) +- **Resumable**: Can be stopped and restarted - only processes services without embeddings +- **Production Safety**: Asks for confirmation in production environments + +### 📊 Migration Output Example +``` +🚀 Starting migration: Generate missing embeddings for services +================================================ +📊 Found 25 services without embeddings +âš ī¸ This process will take time due to API rate limits (15 requests/minute) +âąī¸ Estimated time: 7 minutes + +đŸ“Ļ Processing batch 1... + +âŗ [1/25] Processing: "Web Development Services" + Service ID: clx1abc... + ✅ Success! Embeddings generated and saved. + âąī¸ Waiting 5 seconds (rate limit)... + +📈 Progress: 5/25 services processed +✅ Successful: 5 | ❌ Failed: 0 +âąī¸ Estimated time remaining: 5 minutes +``` + +## How Embeddings Work + +1. **Title Embedding**: Generated from service title +2. **Description Embedding**: Generated from service description +3. **Tags Embedding**: Generated from combined tags +4. **Combined Embedding**: Weighted combination of all three for optimal search + +## API Integration + +The embedding generation is now seamlessly integrated into: + +- ✅ **Service Creation**: `POST /api/services` +- ✅ **Service Updates**: `PUT /api/services/:id` +- ✅ **Semantic Search**: `GET /api/services/search` +- ✅ **Similar Services**: `GET /api/services/:id/similar` +- ✅ **Manual Embedding Updates**: `POST /api/services/:id/embeddings` + +## Database Schema + +The following fields are now automatically populated: + +```sql +-- Service table embedding fields +titleEmbedding vector(768) -- From service title +descriptionEmbedding vector(768) -- From service description +tagsEmbedding vector(768) -- From service tags +combinedEmbedding vector(768) -- Weighted combination +embeddingUpdatedAt DateTime -- Last update timestamp +``` + +## Troubleshooting + +### Common Issues + +1. **API Quota Exceeded** + ``` + Error: Daily API limit reached + ``` + **Solution**: Wait 24 hours or upgrade to paid tier + +2. **Database Connection Issues** + ``` + Error: Database connection failed + ``` + **Solution**: Check DATABASE_URL and database status + +3. **pgvector Extension Missing** + ``` + Error: pgvector extension is not installed + ``` + **Solution**: Install pgvector in PostgreSQL + +### Monitoring Migration Progress + +The migration script provides detailed logging: +- Individual service processing status +- Batch progress updates +- Error details for failed services +- Overall completion statistics + +### Rerunning Migration + +The script is safe to run multiple times: +- Only processes services without embeddings +- Skips services that already have embeddings +- Can resume after interruption + +## Performance Considerations + +- **Rate Limiting**: 5-second delays between API calls +- **Batch Processing**: 5 services per batch (free tier optimized) +- **Memory Efficient**: Processes services in small batches +- **Error Recovery**: Continues processing even if individual services fail + +## Next Steps + +1. **Run Migration**: Execute the migration script for existing services +2. **Monitor Performance**: Check search quality improvement +3. **Optimize**: Fine-tune similarity thresholds based on search results +4. **Scale**: Consider upgrading to paid API tier for higher quotas + +--- + +🎉 **Your semantic search functionality should now work perfectly!** + +All new services will automatically have embeddings, and the migration script will handle existing services. The vector search will now return relevant results based on semantic similarity rather than simple text matching. \ No newline at end of file diff --git a/GEOLOCATION_API_REFERENCE.md b/GEOLOCATION_API_REFERENCE.md new file mode 100755 index 0000000..8c545c5 --- /dev/null +++ b/GEOLOCATION_API_REFERENCE.md @@ -0,0 +1,113 @@ +# 📍 Geolocation API Endpoints Reference + +This document summarizes all backend API endpoints related to geolocation and location-based service features. + +--- + +## 1. Service Creation & Update + +### **POST `/api/services`** +- **Purpose:** Create a new service (supports optional location fields). +- **Body Example:** + ```json + { + "title": "Plumbing Service", + "description": "Fix leaks and more", + "latitude": 6.9271, + "longitude": 79.8612, + "address": "123 Main St, Colombo", + "city": "Colombo", + "state": "Western", + "country": "Sri Lanka", + "postalCode": "10000", + "serviceRadiusKm": 15 + // ...other fields + } + ``` + +### **PUT `/api/services/:id`** +- **Purpose:** Update an existing service, including location fields. + +--- + +## 2. Service Search with Location + +### **GET `/api/services/search`** +- **Purpose:** Search for services by keyword and/or location. +- **Query Parameters:** + - `lat`, `lng` (or `latitude`, `longitude`): User coordinates + - `radius`: Search radius in km (default: 10) + - `address`: (optional) Address to geocode and use as search center + - `keyword`: (optional) Service search keyword +- **Behavior:** + - If no location is provided, returns all services matching the keyword. + - Services without location are considered available everywhere. + +--- + +## 3. Geocoding & Reverse Geocoding + +### **POST `/api/services/location/geocode`** +- **Purpose:** Convert an address to latitude/longitude and structured address fields. +- **Body:** + ```json + { "address": "123 Main St, Colombo" } + ``` +- **Response:** + ```json + { + "latitude": 6.9271, + "longitude": 79.8612, + "address": "123 Main St, Colombo", + "city": "Colombo", + "state": "Western", + "country": "Sri Lanka", + "postalCode": "10000" + } + ``` + +### **POST `/api/services/location/reverse-geocode`** +- **Purpose:** Convert latitude/longitude to a structured address. +- **Body:** + ```json + { "lat": 6.9271, "lng": 79.8612 } + ``` +- **Response:** Same as above. + +--- + +## 4. IP-based Location Detection + +### **GET `/api/services/location/ip`** +- **Purpose:** Detect approximate user location based on IP address. +- **Response:** + ```json + { + "latitude": 6.9271, + "longitude": 79.8612, + "city": "Colombo", + "country": "Sri Lanka", + "accuracy": "approximate" + } + ``` + +--- + +## 5. Other Related Endpoints + +### **GET `/api/services/:id`** +- **Purpose:** Fetch a single service, including its location fields. + +### **GET `/api/services`** +- **Purpose:** List all services (optionally filtered), including location fields. + +--- + +## Notes + +- **Services without location**: Treated as available everywhere and always included in search results. +- **Location fields are optional**: If not provided, the service is not location-restricted. + +--- + +**Keep this section for quick reference when developing or debugging location-based features!** diff --git a/README.md b/README.md deleted file mode 100644 index 075b652..0000000 --- a/README.md +++ /dev/null @@ -1,79 +0,0 @@ - - -## Setup Guide -## 🛑 define .env Before run the npm install - -## 1. Environment Variables (.env) - -Create a `.env` file in your project root with the following format: - -``` -DATABASE_URL="postgresql://:@:/" -``` -Example: -``` -DATABASE_URL="postgresql://postgres:253248@localhost:5432/Zia" -``` - -## 2. Initialize Database & Migrations - -To set up your database tables for the first time, run: -``` -npx prisma migrate dev --name init -``` -This will: -- Create the initial migration based on your Prisma schema -- Apply the migration to your database -- Generate the Prisma Client for your models - -## 3. Generate Prisma Client - -If you change your schema or want to regenerate the client, run: -``` -npx prisma generate -``` -This will generate the Prisma Client in `node_modules/@prisma/client` for use in your code. - -## 4. Seeding the Database - -To insert initial data (seed), run: -``` -npx prisma db seed -``` -This will execute the seed script defined in your `package.json` (usually `prisma/seed.js` or `prisma/seed.ts`). - -## 5. Updating the Database After Schema Changes - -If you edit your Prisma model in `prisma/schema.prisma`, you must create and apply a new migration: -``` -npx prisma migrate dev --name -``` -Replace `` with a descriptive name for your change (e.g., add-user-age). -This will: -- Create a migration file for your changes -- Apply the migration to your database -- Update the Prisma Client - -## Summary of Commands - -- **Initialize database:** - ``` - npx prisma migrate dev --name init - ``` -- **Generate Prisma Client:** - ``` - npx prisma generate - ``` -- **Seed database:** - ``` - npx prisma db seed - ``` -- **Apply schema changes:** - ``` - npx prisma migrate dev --name - ``` - -## Notes -- Always update your `.env` file with the correct database connection string before running migrations or seeds. -- After editing your schema, always run a migration and regenerate the Prisma Client. - diff --git a/SERVICES_API.md b/SERVICES_API.md deleted file mode 100644 index 253e97c..0000000 --- a/SERVICES_API.md +++ /dev/null @@ -1,286 +0,0 @@ -# Services API Documentation - -## Overview -This API provides endpoints for managing services in the application. Services are offerings provided by service providers and can be booked by users. - -## Base URL -``` -http://localhost:3000/api/services -``` - -## Endpoints - -### 1. Create a Service -**POST** `/api/services` - -Creates a new service. - -#### Request Body: -```json -{ - "providerId": "cuid_provider_id", - "categoryId": "cuid_category_id", - "title": "Web Development Service", - "description": "Professional web development services for modern businesses", - "price": 99.99, - "currency": "USD", - "tags": ["web", "development", "react", "nodejs"], - "images": [ - "https://example.com/image1.jpg", - "https://example.com/image2.jpg" - ], - "isActive": true, - "workingTime": [ - "Monday: 9:00 AM - 5:00 PM", - "Tuesday: 9:00 AM - 5:00 PM", - "Wednesday: 9:00 AM - 5:00 PM", - "Thursday: 9:00 AM - 5:00 PM", - "Friday: 9:00 AM - 5:00 PM" - ] -} -``` - -#### Response (201): -```json -{ - "success": true, - "message": "Service created successfully", - "data": { - "id": "cuid_service_id", - "providerId": "cuid_provider_id", - "categoryId": "cuid_category_id", - "title": "Web Development Service", - "description": "Professional web development services for modern businesses", - "price": 99.99, - "currency": "USD", - "tags": ["web", "development", "react", "nodejs"], - "images": ["https://example.com/image1.jpg"], - "isActive": true, - "workingTime": ["Monday: 9:00 AM - 5:00 PM"], - "createdAt": "2025-08-04T12:00:00.000Z", - "updatedAt": "2025-08-04T12:00:00.000Z", - "provider": { - "id": "cuid_provider_id", - "user": { - "firstName": "John", - "lastName": "Doe", - "email": "john@example.com" - } - }, - "category": { - "id": "cuid_category_id", - "name": "Web Development" - } - } -} -``` - -### 2. Get All Services -**GET** `/api/services` - -Retrieves all services with optional filtering and pagination. - -#### Query Parameters: -- `providerId` (optional): Filter by provider ID -- `categoryId` (optional): Filter by category ID -- `isActive` (optional): Filter by active status (`true` or `false`) -- `skip` (optional): Number of records to skip (default: 0) -- `take` (optional): Number of records to return (default: 10, max: 100) - -#### Example: -``` -GET /api/services?categoryId=cuid_category_id&isActive=true&skip=0&take=5 -``` - -#### Response (200): -```json -{ - "success": true, - "message": "Services retrieved successfully", - "data": [ - { - "id": "cuid_service_id", - "title": "Web Development Service", - "price": 99.99, - "provider": { - "user": { - "firstName": "John", - "lastName": "Doe" - } - }, - "category": { - "name": "Web Development" - } - } - ], - "pagination": { - "skip": 0, - "take": 10 - } -} -``` - -### 3. Get Service by ID -**GET** `/api/services/:id` - -Retrieves a specific service by its ID. - -#### Example: -``` -GET /api/services/cuid_service_id -``` - -#### Response (200): -```json -{ - "success": true, - "message": "Service retrieved successfully", - "data": { - "id": "cuid_service_id", - "title": "Web Development Service", - "description": "Professional web development services", - "price": 99.99, - "provider": { - "user": { - "firstName": "John", - "lastName": "Doe", - "email": "john@example.com", - "phone": "+1234567890" - } - }, - "category": { - "name": "Web Development" - }, - "reviews": [], - "schedules": [] - } -} -``` - -### 4. Update Service -**PUT** `/api/services/:id` - -Updates an existing service. - -#### Request Body (all fields optional): -```json -{ - "title": "Updated Web Development Service", - "description": "Updated description", - "price": 149.99, - "tags": ["web", "development", "react", "nodejs", "typescript"], - "isActive": false -} -``` - -#### Response (200): -```json -{ - "success": true, - "message": "Service updated successfully", - "data": { - // Updated service object - } -} -``` - -### 5. Delete Service -**DELETE** `/api/services/:id` - -Deletes a service. - -#### Example: -``` -DELETE /api/services/cuid_service_id -``` - -#### Response (200): -```json -{ - "success": true, - "message": "Service deleted successfully" -} -``` - -## Error Responses - -### Validation Error (400): -```json -{ - "success": false, - "message": "Validation failed", - "errors": [ - { - "field": "price", - "message": "Price is required" - } - ] -} -``` - -### Not Found Error (404): -```json -{ - "success": false, - "message": "Service not found" -} -``` - -### Server Error (500): -```json -{ - "success": false, - "message": "Failed to create service: Provider not found" -} -``` - -## Field Validations - -### Required Fields (for creation): -- `providerId`: Must be a valid provider ID -- `categoryId`: Must be a valid category ID -- `price`: Must be a positive number - -### Optional Fields: -- `title`: 3-100 characters -- `description`: 10-1000 characters -- `currency`: 3-character uppercase code (default: "USD") -- `tags`: Array of strings, max 10 items, each 2-30 characters -- `images`: Array of valid URLs, max 5 items -- `isActive`: Boolean (default: true) -- `workingTime`: Array of formatted time slots, max 7 items - -### Working Time Format: -``` -"Day: HH:MM AM/PM - HH:MM AM/PM" -``` -Examples: -- "Monday: 9:00 AM - 5:00 PM" -- "Saturday: 10:00 AM - 2:00 PM" - -## Testing with curl - -### Create a service: -```bash -curl -X POST http://localhost:3000/api/services \ - -H "Content-Type: application/json" \ - -d '{ - "providerId": "your_provider_id", - "categoryId": "your_category_id", - "title": "Test Service", - "description": "This is a test service description", - "price": 50.00, - "tags": ["test", "service"], - "workingTime": ["Monday: 9:00 AM - 5:00 PM"] - }' -``` - -### Get all services: -```bash -curl http://localhost:3000/api/services -``` - -### Get a specific service: -```bash -curl http://localhost:3000/api/services/your_service_id -``` diff --git a/TESTING.md b/TESTING.md new file mode 100755 index 0000000..4a159cd --- /dev/null +++ b/TESTING.md @@ -0,0 +1,360 @@ +# Testing Guide + +This document provides comprehensive information about the testing setup and practices for the Task Management System backend. + +## Overview + +The testing suite is built using **Jest** with TypeScript support, providing comprehensive coverage for: +- **API endpoints** (integration tests) +- **Service layer** (unit tests) +- **Database operations** (with test database) +- **Authentication & authorization** +- **Error handling** +- **Validation** + +## Test Structure + +``` +tests/ +├── setup.ts # Global test configuration +├── helpers.ts # Test utilities and data factories +├── user.test.ts # User authentication & profile tests +├── task.test.ts # Task CRUD and management tests +├── team.test.ts # Team collaboration tests +├── activity.test.ts # Activity tracking tests +├── comment.test.ts # Comment system tests +├── dependency.test.ts # Task dependency tests +└── services/ + └── user.service.test.ts # Service layer unit tests +``` + +## Setup + +### Environment Configuration + +1. **Test Database**: Uses a separate PostgreSQL database for testing +2. **Environment Variables**: Configured in `.env.test` +3. **Database Migrations**: Automatically applied before tests + +### Installation + +```bash +cd backend +npm install +``` + +### Database Setup + +```bash +# Create test database (one-time setup) +createdb taskmanagement_test + +# Run migrations for test database +npm run test:db:setup +``` + +## Running Tests + +### Basic Commands + +```bash +# Run all tests +npm test + +# Run tests in watch mode +npm run test:watch + +# Run tests with coverage +npm run test:coverage + +# Run tests for CI/CD +npm run test:ci +``` + +### Advanced Commands + +```bash +# Run specific test categories +npm run test:unit # Unit tests only +npm run test:integration # Integration tests only +npm run test:services # Service layer tests only + +# Run comprehensive test suite +npm run test:full # Full test suite with cleanup + +# Quick testing (skip linting/type checking) +npm run test:quick + +# Reset test database +npm run test:db:reset +``` + +### Custom Test Runner + +The project includes a custom test runner (`test-runner.js`) with additional features: + +```bash +# Run with custom options +node test-runner.js --coverage --verbose --cleanup + +# Available options: +# --coverage Generate coverage report +# --verbose Detailed output +# --watch Watch mode +# --cleanup Clean up after tests +# --skip-lint Skip ESLint +# --skip-type-check Skip TypeScript compilation +``` + +## Test Patterns + +### API Testing Pattern + +```typescript +describe('POST /api/endpoint', () => { + it('should create resource successfully', async () => { + const testData = { + name: 'Test Resource', + description: 'Test Description' + }; + + const response = await request(app) + .post('/api/endpoint') + .set('Authorization', `Bearer ${authToken}`) + .send(testData) + .expect(201); + + expect(response.body).toMatchObject({ + name: testData.name, + description: testData.description + }); + }); +}); +``` + +### Service Testing Pattern + +```typescript +describe('UserService', () => { + it('should create user with hashed password', async () => { + const userData = { + username: 'testuser', + email: 'test@example.com', + password: 'password123' + }; + + const user = await userService.createUser(userData); + + expect(user.username).toBe(userData.username); + expect(user.password).toBeUndefined(); // Password excluded from response + + // Verify password was hashed in database + const dbUser = await prisma.user.findUnique({ + where: { id: user.id } + }); + expect(dbUser?.password).not.toBe(userData.password); + }); +}); +``` + +### Authentication Testing + +```typescript +// Using test helpers +const { user, authToken } = await createAuthenticatedUser(); + +const response = await request(app) + .get('/api/protected-endpoint') + .set('Authorization', `Bearer ${authToken}`) + .expect(200); +``` + +## Test Helpers + +### Data Factories + +The `helpers.ts` file provides factory functions for creating test data: + +```typescript +// Create test user +const user = await createTestUser({ + username: 'customuser', + email: 'custom@example.com' +}); + +// Create authenticated user with token +const { user, authToken } = await createAuthenticatedUser(); + +// Create test team with members +const { team, owner } = await createTestTeam({ + name: 'Test Team', + memberCount: 3 +}); + +// Create test task +const task = await createTestTask({ + title: 'Test Task', + assigneeId: user.id, + teamId: team.id +}); +``` + +### Validation Helpers + +```typescript +// Test input validation +await expectValidationError( + request(app).post('/api/users').send(invalidData), + ['email', 'password'] +); + +// Test authentication requirement +await expectAuthenticationError( + request(app).get('/api/protected-endpoint') +); +``` + +## Coverage Requirements + +The project maintains high test coverage standards: + +- **Statements**: > 90% +- **Branches**: > 85% +- **Functions**: > 90% +- **Lines**: > 90% + +### Coverage Reports + +```bash +# Generate and view coverage +npm run test:coverage +open coverage/lcov-report/index.html +``` + +## Database Testing + +### Test Database Isolation + +- Each test file uses transactions that are rolled back +- Database is cleaned between test suites +- Separate test database prevents data conflicts + +### Migration Testing + +```bash +# Test migrations on clean database +npm run test:db:reset +npm run test:db:setup +``` + +## CI/CD Integration + +### GitHub Actions + +The project includes a comprehensive CI/CD workflow (`.github/workflows/test.yml`): + +- **Multi-node testing** (Node.js 18.x, 20.x) +- **PostgreSQL service** for database tests +- **Code coverage** reporting +- **Artifact archiving** for test results + +### Local CI Simulation + +```bash +# Run tests as they would in CI +npm run test:ci +``` + +## Debugging Tests + +### Common Issues + +1. **Database Connection**: Ensure test database exists and is accessible +2. **Environment Variables**: Check `.env.test` configuration +3. **Port Conflicts**: Ensure test ports are available +4. **Migration State**: Reset database if migrations are out of sync + +### Debug Commands + +```bash +# Debug database connectivity +npm run test:db:reset + +# Run single test file +npx jest tests/user.test.ts + +# Run tests with debug output +DEBUG=* npm test + +# Run specific test case +npx jest -t "should create user successfully" +``` + +### VS Code Debug Configuration + +Add to `.vscode/launch.json`: + +```json +{ + "name": "Debug Jest Tests", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": ["--runInBand"], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "cwd": "${workspaceFolder}/backend" +} +``` + +## Best Practices + +### Writing Tests + +1. **Descriptive Names**: Use clear, descriptive test names +2. **Arrange-Act-Assert**: Follow the AAA pattern +3. **Test Isolation**: Each test should be independent +4. **Edge Cases**: Test boundary conditions and error scenarios +5. **Mock External Dependencies**: Use mocks for external services + +### Test Organization + +1. **Group Related Tests**: Use `describe` blocks for logical grouping +2. **Setup/Teardown**: Use `beforeEach`/`afterEach` for test preparation +3. **Shared Utilities**: Extract common test logic to helpers +4. **Data Management**: Use factories for consistent test data + +### Performance + +1. **Parallel Execution**: Tests run in parallel by default +2. **Database Cleanup**: Efficient cleanup between tests +3. **Resource Management**: Proper cleanup of resources +4. **Test Selection**: Use focused testing during development + +## Troubleshooting + +### Common Errors + +| Error | Solution | +|-------|----------| +| `Database connection failed` | Check `.env.test` and ensure test database exists | +| `Port already in use` | Kill processes using test ports or change port configuration | +| `Migration failed` | Reset database with `npm run test:db:reset` | +| `Timeout errors` | Increase Jest timeout in `jest.config.js` | +| `Authentication failed` | Verify JWT secret and token generation | + +### Getting Help + +1. Check test output for specific error messages +2. Verify environment configuration +3. Run tests in verbose mode: `npm run test:verbose` +4. Check database logs for connection issues +5. Review Jest documentation for advanced configuration + +## Future Enhancements + +- **E2E Testing**: Browser automation with Playwright +- **Performance Testing**: Load testing with Artillery +- **Visual Testing**: Screenshot comparison testing +- **Contract Testing**: API contract validation +- **Mutation Testing**: Code quality validation with Stryker \ No newline at end of file diff --git a/add-location-support.sql b/add-location-support.sql new file mode 100755 index 0000000..1367a49 --- /dev/null +++ b/add-location-support.sql @@ -0,0 +1,46 @@ +-- Enable PostGIS extension +CREATE EXTENSION IF NOT EXISTS postgis; + +-- Add location columns to services table +ALTER TABLE "Service" +ADD COLUMN IF NOT EXISTS latitude DOUBLE PRECISION, +ADD COLUMN IF NOT EXISTS longitude DOUBLE PRECISION, +ADD COLUMN IF NOT EXISTS address TEXT, +ADD COLUMN IF NOT EXISTS city VARCHAR(100), +ADD COLUMN IF NOT EXISTS state VARCHAR(100), +ADD COLUMN IF NOT EXISTS country VARCHAR(100), +ADD COLUMN IF NOT EXISTS "postalCode" VARCHAR(20), +ADD COLUMN IF NOT EXISTS "serviceRadiusKm" DOUBLE PRECISION DEFAULT 10, +ADD COLUMN IF NOT EXISTS "locationLastUpdated" TIMESTAMP(3); + +-- Create location column using PostGIS POINT type +ALTER TABLE "Service" +ADD COLUMN IF NOT EXISTS location GEOGRAPHY(POINT, 4326); + +-- Create spatial index for faster queries +CREATE INDEX IF NOT EXISTS idx_services_location ON "Service" USING GIST(location); + +-- Create regular indexes for location fields +CREATE INDEX IF NOT EXISTS idx_services_lat_lng ON "Service" (latitude, longitude); +CREATE INDEX IF NOT EXISTS idx_services_city ON "Service" (city); +CREATE INDEX IF NOT EXISTS idx_services_state ON "Service" (state); + +-- Function to update geography column when lat/lng changes +CREATE OR REPLACE FUNCTION update_service_location() +RETURNS TRIGGER AS $$ +BEGIN + IF NEW.latitude IS NOT NULL AND NEW.longitude IS NOT NULL THEN + NEW.location = ST_SetSRID(ST_MakePoint(NEW.longitude, NEW.latitude), 4326)::geography; + NEW."locationLastUpdated" = NOW(); + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create trigger to automatically update location column +DROP TRIGGER IF EXISTS trigger_update_service_location ON "Service"; +CREATE TRIGGER trigger_update_service_location + BEFORE INSERT OR UPDATE OF latitude, longitude + ON "Service" + FOR EACH ROW + EXECUTE FUNCTION update_service_location(); \ No newline at end of file diff --git a/category_dataset.json b/category_dataset.json deleted file mode 100644 index 911d0fe..0000000 --- a/category_dataset.json +++ /dev/null @@ -1,294 +0,0 @@ -{ - "categories": [ - { - "name": "Home Services", - "slug": "home-services", - "description": "Professional services for your home maintenance, repairs, and improvements", - "parentId": null, - "subcategories": [ - { - "name": "Cleaning Services", - "slug": "cleaning-services", - "description": "Professional cleaning for homes and offices" - }, - { - "name": "Plumbing", - "slug": "plumbing", - "description": "Plumbing repairs, installations, and maintenance" - }, - { - "name": "Electrical Work", - "slug": "electrical-work", - "description": "Electrical installations, repairs, and maintenance" - }, - { - "name": "Gardening & Landscaping", - "slug": "gardening-landscaping", - "description": "Garden maintenance, landscaping, and outdoor services" - }, - { - "name": "Home Renovation", - "slug": "home-renovation", - "description": "Home improvement and renovation services" - }, - { - "name": "Painting & Decorating", - "slug": "painting-decorating", - "description": "Interior and exterior painting services" - }, - { - "name": "Carpentry", - "slug": "carpentry", - "description": "Custom woodwork and carpentry services" - }, - { - "name": "Pest Control", - "slug": "pest-control", - "description": "Professional pest management and control services" - }, - { - "name": "HVAC Services", - "slug": "hvac-services", - "description": "Heating, ventilation, and air conditioning services" - }, - { - "name": "Security Systems", - "slug": "security-systems", - "description": "Home security installation and monitoring" - } - ] - }, - { - "name": "Creative Services", - "slug": "creative-services", - "description": "Professional creative and design services for businesses and individuals", - "parentId": null, - "subcategories": [ - { - "name": "Graphic Design", - "slug": "graphic-design", - "description": "Logo design, branding, and visual identity services" - }, - { - "name": "Web Design", - "slug": "web-design", - "description": "Website design and user interface development" - }, - { - "name": "Photography", - "slug": "photography", - "description": "Professional photography for events, portraits, and commercial use" - }, - { - "name": "Video Production", - "slug": "video-production", - "description": "Video editing, production, and cinematography services" - }, - { - "name": "Content Writing", - "slug": "content-writing", - "description": "Professional writing services for web, marketing, and publications" - }, - { - "name": "Digital Marketing", - "slug": "digital-marketing", - "description": "Social media management, SEO, and online marketing" - }, - { - "name": "Animation", - "slug": "animation", - "description": "2D and 3D animation services" - }, - { - "name": "Voice Over", - "slug": "voice-over", - "description": "Professional voice recording and narration services" - }, - { - "name": "Music Production", - "slug": "music-production", - "description": "Audio recording, mixing, and music production" - }, - { - "name": "Illustration", - "slug": "illustration", - "description": "Custom illustrations and artwork" - } - ] - }, - { - "name": "Personal Care", - "slug": "personal-care", - "description": "Health, wellness, and personal grooming services", - "parentId": null, - "subcategories": [ - { - "name": "Hair Styling", - "slug": "hair-styling", - "description": "Professional hair cutting, styling, and treatments" - }, - { - "name": "Beauty & Makeup", - "slug": "beauty-makeup", - "description": "Makeup services for events and special occasions" - }, - { - "name": "Spa Services", - "slug": "spa-services", - "description": "Relaxation and wellness spa treatments" - }, - { - "name": "Massage Therapy", - "slug": "massage-therapy", - "description": "Therapeutic and relaxation massage services" - }, - { - "name": "Nail Care", - "slug": "nail-care", - "description": "Manicure, pedicure, and nail art services" - }, - { - "name": "Personal Training", - "slug": "personal-training", - "description": "Fitness coaching and personal training sessions" - }, - { - "name": "Nutrition Counseling", - "slug": "nutrition-counseling", - "description": "Dietary advice and nutrition planning" - }, - { - "name": "Mental Health Counseling", - "slug": "mental-health-counseling", - "description": "Professional counseling and therapy services" - }, - { - "name": "Yoga & Meditation", - "slug": "yoga-meditation", - "description": "Yoga classes and meditation guidance" - }, - { - "name": "Skincare", - "slug": "skincare", - "description": "Professional skincare treatments and consultations" - } - ] - }, - { - "name": "Professional Services", - "slug": "professional-services", - "description": "Expert professional and consulting services", - "parentId": null, - "subcategories": [ - { - "name": "Legal Services", - "slug": "legal-services", - "description": "Legal consultation and representation" - }, - { - "name": "Accounting & Bookkeeping", - "slug": "accounting-bookkeeping", - "description": "Financial management and accounting services" - }, - { - "name": "Tax Preparation", - "slug": "tax-preparation", - "description": "Tax filing and preparation services" - }, - { - "name": "Real Estate", - "slug": "real-estate", - "description": "Property buying, selling, and rental services" - }, - { - "name": "Insurance Services", - "slug": "insurance-services", - "description": "Insurance consultation and policy management" - }, - { - "name": "Financial Planning", - "slug": "financial-planning", - "description": "Investment advice and financial planning" - }, - { - "name": "Architecture", - "slug": "architecture", - "description": "Architectural design and planning services" - }, - { - "name": "Engineering", - "slug": "engineering", - "description": "Professional engineering consultation" - }, - { - "name": "Translation Services", - "slug": "translation-services", - "description": "Document translation and interpretation" - }, - { - "name": "Notary Services", - "slug": "notary-services", - "description": "Document notarization and certification" - } - ] - }, - { - "name": "Business Services", - "slug": "business-services", - "description": "Professional services to help businesses grow and operate efficiently", - "parentId": null, - "subcategories": [ - { - "name": "Business Consulting", - "slug": "business-consulting", - "description": "Strategic business advice and consulting" - }, - { - "name": "IT Support", - "slug": "it-support", - "description": "Computer and network technical support" - }, - { - "name": "Software Development", - "slug": "software-development", - "description": "Custom software and application development" - }, - { - "name": "Data Entry", - "slug": "data-entry", - "description": "Professional data entry and processing services" - }, - { - "name": "Virtual Assistant", - "slug": "virtual-assistant", - "description": "Remote administrative and support services" - }, - { - "name": "Customer Service", - "slug": "customer-service", - "description": "Outsourced customer support services" - }, - { - "name": "HR Services", - "slug": "hr-services", - "description": "Human resources consulting and management" - }, - { - "name": "Project Management", - "slug": "project-management", - "description": "Professional project planning and execution" - }, - { - "name": "Market Research", - "slug": "market-research", - "description": "Market analysis and research services" - }, - { - "name": "Business Writing", - "slug": "business-writing", - "description": "Professional business documents and proposals" - } - ] - } - ] -} diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 2f0cb57..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,9 +0,0 @@ - -services: - api: - build: . - ports: - - "3000:3000" - env_file: - - .env - restart: unless-stopped diff --git a/dockerfile b/dockerfile deleted file mode 100644 index 74638c3..0000000 --- a/dockerfile +++ /dev/null @@ -1,24 +0,0 @@ - - -FROM node:18-alpine - -# App directory -WORKDIR /app - - -# Install dependencies first (better cache use) -COPY package*.json ./ -RUN npm install - -# Prisma client -COPY prisma ./prisma -RUN npx prisma generate - -# Rest of the source -COPY . . - -# Expose the service port -EXPOSE 3000 - -# Start the API -CMD ["npm", "start"] diff --git a/index.js b/index.js deleted file mode 100644 index 5be8525..0000000 --- a/index.js +++ /dev/null @@ -1,43 +0,0 @@ - -import 'dotenv/config'; -import { execSync } from 'node:child_process'; - -try { - console.log('Running `prisma generate` â€Ļ'); - execSync('npx prisma generate', { stdio: 'inherit' }); -} catch (err) { - console.error('Could not run `prisma generate`:', err); - process.exit(1); -} - -import { PrismaClient } from '@prisma/client'; -import { withAccelerate } from '@prisma/extension-accelerate'; -import express from 'express'; -import cors from 'cors'; -import userRoutes from './src/routes/user.route.js'; -import providerRoutes from './src/routes/provider.route.js'; -import companyRoutes from './src/routes/company.route.js'; -import servicesRoutes from './src/routes/services.route.js'; -import categoryRoutes from './src/routes/catagory.route.js'; - -const prisma = new PrismaClient().$extends(withAccelerate()); - -const app = express(); - -// CORS configuration -app.use(cors({ - origin: ['http://localhost:5173', 'http://localhost:3000'], // Allow both frontend and backend origins - methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], - allowedHeaders: ['Content-Type', 'Authorization'], - credentials: true // Allow cookies if needed -})); - -app.use(express.json()); -app.use('/api/users', userRoutes); -app.use('/api/providers', providerRoutes); -app.use('/api/companies', companyRoutes); -app.use('/api/services', servicesRoutes); -app.use('/api/categories', categoryRoutes); - -const PORT = process.env.PORT || 3000; -app.listen(PORT, () => console.log(`Server running on port ${PORT}`)); diff --git a/index.ts b/index.ts new file mode 100755 index 0000000..8701ab9 --- /dev/null +++ b/index.ts @@ -0,0 +1,113 @@ +import 'dotenv/config'; +import { execSync } from 'node:child_process'; + +try { + console.log('Running `prisma generate` â€Ļ'); + execSync('npx prisma generate', { stdio: 'inherit' }); +} catch (err) { + console.error('Could not run `prisma generate`:', err); + process.exit(1); +} + +import { prisma } from './src/utils/database.js'; +import { queueService } from './src/services/queue.service.js'; +import express, { type Application } from 'express'; +import cors, { type CorsOptions } from 'cors'; +import rateLimit from 'express-rate-limit'; +import userRoutes from './src/routes/user.route.js'; +import providerRoutes from './src/routes/provider.route.js'; +import companyRoutes from './src/routes/company.route.js'; +import servicesRoutes from './src/routes/services.route.js'; +import categoryRoutes from './src/routes/category.route.js'; +import adminRoutes from './src/Admin/routes/admin.route.js'; +import confirmationRoutes from './src/routes/confirmation.route.js'; +import reviewRoutes from './src/routes/review.route.js'; +import serviceReviewRoutes from './src/routes/serviceReview.route.js'; +import { chatbotRoutes, CHATBOT_MODULE_INFO } from './src/modules/chatbot/index.js'; + +// Simple database test function +async function testDatabaseConnection() { + try { + await prisma.$queryRaw`SELECT 1 as test`; + console.log('✅ Database connection successful'); + return true; + } catch (error: any) { + console.error('❌ Database connection failed:', error.message); + return false; + } +} + +const app: Application = express(); + +// CORS configuration (must run before any rate limiting or routes) +const corsOptions: CorsOptions = { + origin: ['http://localhost:5173', 'http://localhost:3000'], + methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization'], + credentials: true, +}; +app.use(cors(corsOptions)); + +// Rate limiting +const limiter = rateLimit({ + windowMs: 30 * 60 * 1000, // 30 minutes + max: 10000, // increased limit for development + message: 'Too many requests from this IP, please try again later.', + standardHeaders: true, + legacyHeaders: false, + // Do not rate-limit CORS preflight requests + skip: (req) => req.method === 'OPTIONS', +}); + +// Apply rate limiting to all routes +app.use(limiter); + +// Increase JSON payload limit for file uploads +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); +app.use('/api/users', userRoutes); +app.use('/api/providers', providerRoutes); +app.use('/api/companies', companyRoutes); +app.use('/api/services', servicesRoutes); +app.use('/api/categories', categoryRoutes); +app.use('/api/admin', adminRoutes); +app.use('/api/confirmations', confirmationRoutes); +app.use('/api/reviews', reviewRoutes); +app.use('/api/service-reviews', serviceReviewRoutes); +app.use('/api/chatbot', chatbotRoutes); + +console.log(`🤖 ${CHATBOT_MODULE_INFO.name} loaded with endpoints:`, CHATBOT_MODULE_INFO.endpoints); + +const PORT: number = parseInt(process.env.PORT || '3000', 10); + +// Start server with basic database test +async function startServer() { + console.log('🚀 Starting server...'); + + const dbConnected = await testDatabaseConnection(); + + if (!dbConnected) { + console.error('đŸ’Ĩ Server startup aborted due to database connection failure'); + process.exit(1); + } + + // Initialize queue service + try { + await queueService.connect(); + queueService.setupGracefulShutdown(); + console.log('✅ RabbitMQ connection established'); + } catch (error) { + console.error('âš ī¸ RabbitMQ connection failed, emails will not be sent:', error); + // Don't exit - continue without email functionality + } + + app.listen(PORT, () => { + console.log(`đŸŽ¯ Server running on port ${PORT}`); + }); +} + +// Start the server +startServer().catch((error) => { + console.error('đŸ’Ĩ Failed to start server:', error); + process.exit(1); +}); diff --git a/jest.config.js b/jest.config.js new file mode 100755 index 0000000..ecb21c9 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,18 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: ['/src', '/tests'], + testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'], + transform: { + '^.+\\.ts$': 'ts-jest', + }, + collectCoverageFrom: [ + 'src/**/*.ts', + '!src/**/*.d.ts', + '!src/index.ts', + ], + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], + setupFilesAfterEnv: ['/tests/setup.ts'], + testTimeout: 30000, +}; \ No newline at end of file diff --git a/nodemon.json b/nodemon.json new file mode 100755 index 0000000..7f51d5b --- /dev/null +++ b/nodemon.json @@ -0,0 +1,6 @@ +{ + "watch": ["src"], + "ext": "ts,json", + "ignore": ["src/**/*.spec.ts"], + "exec": "ts-node src/index.ts" +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json old mode 100644 new mode 100755 index 036a584..eaa4d5e --- a/package-lock.json +++ b/package-lock.json @@ -9,960 +9,5302 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@prisma/client": "^6.12.0", - "@prisma/extension-accelerate": "^2.0.2", + "@aws-sdk/client-s3": "^3.873.0", + "@aws-sdk/client-ses": "^3.872.0", + "@aws-sdk/s3-request-presigner": "^3.873.0", + "@googlemaps/google-maps-services-js": "^3.4.2", + "@prisma/client": "6.15.0", + "@types/amqplib": "^0.10.7", + "@types/aws-sdk": "^2.7.4", + "@types/multer": "^2.0.0", + "@types/pg": "^8.15.5", + "amqplib": "^0.10.9", + "aws-sdk": "^2.1692.0", + "axios": "^1.12.2", "bcrypt": "^6.0.0", "cors": "^2.8.5", "dotenv": "^17.2.1", "express": "^5.1.0", + "express-rate-limit": "^8.0.1", "joi": "^17.13.3", "jsonwebtoken": "^9.0.2", + "multer": "^2.0.2", "nodemon": "^3.1.10", - "prisma": "^6.12.0" + "pg": "^8.16.3", + "prisma": "^6.12.0", + "socket.io-client": "^4.8.1" }, "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", + "@types/jest": "^30.0.0", + "@types/node": "^24.5.2", + "@types/socket.io": "^3.0.1", + "@types/supertest": "^6.0.3", + "cors": "^2.8.5", + "jest": "^30.1.3", + "nodemon": "^3.1.10", + "rimraf": "^6.0.1", + "supertest": "^7.1.4", + "ts-jest": "^29.4.3", "ts-node": "^10.9.2", - "typescript": "^5.8.3" + "typescript": "^5.9.2" } }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { - "node": ">=12" + "node": ">=6.9.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==", - "license": "BSD-3-Clause" + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } }, - "node_modules/@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "license": "BSD-3-Clause", + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", "dependencies": { - "@hapi/hoek": "^9.0.0" + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" } }, - "node_modules/@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, - "license": "MIT", + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dependencies": { + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.0.0" + "node": ">=6.9.0" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", - "dev": true, - "license": "MIT" + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@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": ">=14.0.0" + } }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" } }, - "node_modules/@prisma/client": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.12.0.tgz", - "integrity": "sha512-wn98bJ3Cj6edlF4jjpgXwbnQIo/fQLqqQHPk2POrZPxTlhY3+n90SSIF3LMRVa8VzRFC/Gec3YKJRxRu+AIGVA==", - "hasInstallScript": true, - "license": "Apache-2.0", + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, "engines": { - "node": ">=18.18" + "node": ">=6.9.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" }, - "peerDependencies": { - "prisma": "*", - "typescript": ">=5.1.0" + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dependencies": { + "tslib": "^2.6.2" }, - "peerDependenciesMeta": { - "prisma": { - "optional": true - }, - "typescript": { - "optional": true - } + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@prisma/config": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.12.0.tgz", - "integrity": "sha512-HovZWzhWEMedHxmjefQBRZa40P81N7/+74khKFz9e1AFjakcIQdXgMWKgt20HaACzY+d1LRBC+L4tiz71t9fkg==", + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.873.0.tgz", + "integrity": "sha512-b+1lSEf+obcC508blw5qEDR1dyTiHViZXbf8G6nFospyqLJS0Vu2py+e+LG2VDVdAouZ8+RvW+uAi73KgsWl0w==", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.873.0", + "@aws-sdk/credential-provider-node": "3.873.0", + "@aws-sdk/middleware-bucket-endpoint": "3.873.0", + "@aws-sdk/middleware-expect-continue": "3.873.0", + "@aws-sdk/middleware-flexible-checksums": "3.873.0", + "@aws-sdk/middleware-host-header": "3.873.0", + "@aws-sdk/middleware-location-constraint": "3.873.0", + "@aws-sdk/middleware-logger": "3.873.0", + "@aws-sdk/middleware-recursion-detection": "3.873.0", + "@aws-sdk/middleware-sdk-s3": "3.873.0", + "@aws-sdk/middleware-ssec": "3.873.0", + "@aws-sdk/middleware-user-agent": "3.873.0", + "@aws-sdk/region-config-resolver": "3.873.0", + "@aws-sdk/signature-v4-multi-region": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.873.0", + "@aws-sdk/util-user-agent-browser": "3.873.0", + "@aws-sdk/util-user-agent-node": "3.873.0", + "@aws-sdk/xml-builder": "3.873.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.8.0", + "@smithy/eventstream-serde-browser": "^4.0.5", + "@smithy/eventstream-serde-config-resolver": "^4.1.3", + "@smithy/eventstream-serde-node": "^4.0.5", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-blob-browser": "^4.0.5", + "@smithy/hash-node": "^4.0.5", + "@smithy/hash-stream-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/md5-js": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-retry": "^4.1.19", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.26", + "@smithy/util-defaults-mode-node": "^4.0.26", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@smithy/util-stream": "^4.2.4", + "@smithy/util-utf8": "^4.0.0", + "@smithy/util-waiter": "^4.0.7", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@aws-sdk/client-ses": { + "version": "3.886.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.886.0.tgz", + "integrity": "sha512-nZ+HNMfhHI0WvXzE7n/LQNgQdKGjL49uKuDd5GxpgOsM5I4sPfU6M+o4Ghrpepca4aynsL9xq+rqQ1+stAEILg==", "license": "Apache-2.0", "dependencies": { - "jiti": "2.4.2" + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.883.0", + "@aws-sdk/credential-provider-node": "3.886.0", + "@aws-sdk/middleware-host-header": "3.873.0", + "@aws-sdk/middleware-logger": "3.876.0", + "@aws-sdk/middleware-recursion-detection": "3.886.0", + "@aws-sdk/middleware-user-agent": "3.883.0", + "@aws-sdk/region-config-resolver": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.879.0", + "@aws-sdk/util-user-agent-browser": "3.873.0", + "@aws-sdk/util-user-agent-node": "3.883.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.9.2", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.21", + "@smithy/middleware-retry": "^4.1.22", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.5.2", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.29", + "@smithy/util-defaults-mode-node": "^4.0.29", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@smithy/util-utf8": "^4.0.0", + "@smithy/util-waiter": "^4.0.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@prisma/debug": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.12.0.tgz", - "integrity": "sha512-plbz6z72orcqr0eeio7zgUrZj5EudZUpAeWkFTA/DDdXEj28YHDXuiakvR6S7sD6tZi+jiwQEJAPeV6J6m/tEQ==", - "license": "Apache-2.0" + "node_modules/@aws-sdk/client-ses/node_modules/@aws-sdk/client-sso": { + "version": "3.886.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.886.0.tgz", + "integrity": "sha512-CwpPZBlONsUO6OvMzNNP9PETZ3dPCQum3nUisk5VuzLTvNd80w2aWeSN/TpcAAbNvcRYbM+FsC4gBm4Q4VWn0g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.883.0", + "@aws-sdk/middleware-host-header": "3.873.0", + "@aws-sdk/middleware-logger": "3.876.0", + "@aws-sdk/middleware-recursion-detection": "3.886.0", + "@aws-sdk/middleware-user-agent": "3.883.0", + "@aws-sdk/region-config-resolver": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.879.0", + "@aws-sdk/util-user-agent-browser": "3.873.0", + "@aws-sdk/util-user-agent-node": "3.883.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.9.2", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.21", + "@smithy/middleware-retry": "^4.1.22", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.5.2", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.29", + "@smithy/util-defaults-mode-node": "^4.0.29", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=6.9.0" + } }, - "node_modules/@prisma/engines": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.12.0.tgz", - "integrity": "sha512-4BRZZUaAuB4p0XhTauxelvFs7IllhPmNLvmla0bO1nkECs8n/o1pUvAVbQ/VOrZR5DnF4HED0PrGai+rIOVePA==", - "hasInstallScript": true, + "node_modules/@aws-sdk/client-ses/node_modules/@aws-sdk/core": { + "version": "3.883.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.883.0.tgz", + "integrity": "sha512-FmkqnqBLkXi4YsBPbF6vzPa0m4XKUuvgKDbamfw4DZX2CzfBZH6UU4IwmjNV3ZM38m0xraHarK8KIbGSadN3wg==", "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.12.0", - "@prisma/engines-version": "6.12.0-15.8047c96bbd92db98a2abc7c9323ce77c02c89dbc", - "@prisma/fetch-engine": "6.12.0", - "@prisma/get-platform": "6.12.0" + "@aws-sdk/types": "3.862.0", + "@aws-sdk/xml-builder": "3.873.0", + "@smithy/core": "^3.9.2", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/property-provider": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/signature-v4": "^5.1.3", + "@smithy/smithy-client": "^4.5.2", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-utf8": "^4.0.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@prisma/engines-version": { - "version": "6.12.0-15.8047c96bbd92db98a2abc7c9323ce77c02c89dbc", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.12.0-15.8047c96bbd92db98a2abc7c9323ce77c02c89dbc.tgz", - "integrity": "sha512-70vhecxBJlRr06VfahDzk9ow4k1HIaSfVUT3X0/kZoHCMl9zbabut4gEXAyzJZxaCGi5igAA7SyyfBI//mmkbQ==", - "license": "Apache-2.0" + "node_modules/@aws-sdk/client-ses/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.883.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.883.0.tgz", + "integrity": "sha512-Z6tPBXPCodfhIF1rvQKoeRGMkwL6TK0xdl1UoMIA1x4AfBpPICAF77JkFBExk/pdiFYq1d04Qzddd/IiujSlLg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.883.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=6.9.0" + } }, - "node_modules/@prisma/extension-accelerate": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@prisma/extension-accelerate/-/extension-accelerate-2.0.2.tgz", - "integrity": "sha512-yZK6/k7uOEFpEsKoZezQS1CKDboPtBCQ0NyI70e1Un8tDiRgg80iWGyjsJmRpps2ZIut3MroHP+dyR3wVKh8lA==", + "node_modules/@aws-sdk/client-ses/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.883.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.883.0.tgz", + "integrity": "sha512-P589ug1lMOOEYLTaQJjSP+Gee34za8Kk2LfteNQfO9SpByHFgGj++Sg8VyIe30eZL8Q+i4qTt24WDCz1c+dgYg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.883.0", + "@aws-sdk/types": "3.862.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/property-provider": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.5.2", + "@smithy/types": "^4.3.2", + "@smithy/util-stream": "^4.2.4", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" + } + }, + "node_modules/@aws-sdk/client-ses/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.886.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.886.0.tgz", + "integrity": "sha512-86ZuuUGLzzYqxkglFBUMCsvb7vSr+IeIPkXD/ERuX9wX0xPxBK961UG7pygO7yaAVzcHSWbWArAXOcEWVlk+7Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.883.0", + "@aws-sdk/credential-provider-env": "3.883.0", + "@aws-sdk/credential-provider-http": "3.883.0", + "@aws-sdk/credential-provider-process": "3.883.0", + "@aws-sdk/credential-provider-sso": "3.886.0", + "@aws-sdk/credential-provider-web-identity": "3.886.0", + "@aws-sdk/nested-clients": "3.886.0", + "@aws-sdk/types": "3.862.0", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-ses/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.886.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.886.0.tgz", + "integrity": "sha512-hyXQrUW6bXkSWOZlNWnNcbXsjM0CBIOfutDFd3tS7Ilhqkx8P3eptT0fVR8GFxNg/ruq5PvnybGK83brUmD7tw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.883.0", + "@aws-sdk/credential-provider-http": "3.883.0", + "@aws-sdk/credential-provider-ini": "3.886.0", + "@aws-sdk/credential-provider-process": "3.883.0", + "@aws-sdk/credential-provider-sso": "3.886.0", + "@aws-sdk/credential-provider-web-identity": "3.886.0", + "@aws-sdk/types": "3.862.0", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@aws-sdk/client-ses/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.883.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.883.0.tgz", + "integrity": "sha512-m1shbHY/Vppy4EdddG9r8x64TO/9FsCjokp5HbKcZvVoTOTgUJrdT8q2TAQJ89+zYIJDqsKbqfrmfwJ1zOdnGQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.883.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" }, "peerDependencies": { - "@prisma/client": ">=4.16.1" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@prisma/fetch-engine": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.12.0.tgz", - "integrity": "sha512-EamoiwrK46rpWaEbLX9aqKDPOd8IyLnZAkiYXFNuq0YsU0Z8K09/rH8S7feOWAVJ3xzeSgcEJtBlVDrajM9Sag==", + "node_modules/@aws-sdk/client-ses/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.886.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.886.0.tgz", + "integrity": "sha512-KxNgGcT/2ec7XBhiYGBYlk+UyiMqosi5LzLjq2qR4nYf8Deo/lCtbqXSQplwSQ0JIV2kNDcnMQiSafSS9TrL/A==", "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.12.0", - "@prisma/engines-version": "6.12.0-15.8047c96bbd92db98a2abc7c9323ce77c02c89dbc", - "@prisma/get-platform": "6.12.0" + "@aws-sdk/client-sso": "3.886.0", + "@aws-sdk/core": "3.883.0", + "@aws-sdk/token-providers": "3.886.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@prisma/get-platform": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.12.0.tgz", - "integrity": "sha512-nRerTGhTlgyvcBlyWgt8OLNIV7QgJS2XYXMJD1hysorMCuLAjuDDuoxmVt7C2nLxbuxbWPp7OuFRHC23HqD9dA==", + "node_modules/@aws-sdk/client-ses/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.886.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.886.0.tgz", + "integrity": "sha512-pilcy1GUOr4lIWApTcgJLGL+t79SOoe66pzmranQhbn+HGAp2VgiZizeID9P3HLmZObStVal4yTaJur0hWb5ZQ==", "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.12.0" + "@aws-sdk/core": "3.883.0", + "@aws-sdk/nested-clients": "3.886.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "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==", - "license": "BSD-3-Clause", + "node_modules/@aws-sdk/client-ses/node_modules/@aws-sdk/middleware-logger": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.876.0.tgz", + "integrity": "sha512-cpWJhOuMSyz9oV25Z/CMHCBTgafDCbv7fHR80nlRrPdPZ8ETNsahwRgltXP1QJJ8r3X/c1kwpOR7tc+RabVzNA==", + "license": "Apache-2.0", "dependencies": { - "@hapi/hoek": "^9.0.0" + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.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==", - "license": "BSD-3-Clause" + "node_modules/@aws-sdk/client-ses/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.886.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.886.0.tgz", + "integrity": "sha512-yMMlPqiX1SXFwQ0L1a/U19rdXx7eYseHsJEC9F9M5LUUPBI7k117nA0vXxvsvODVQ6JKtY7nTiPrc98GcVKgnw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@aws/lambda-invoke-store": "^0.0.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } }, - "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==", - "license": "BSD-3-Clause" + "node_modules/@aws-sdk/client-ses/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.883.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.883.0.tgz", + "integrity": "sha512-q58uLYnGLg7hsnWpdj7Cd1Ulsq1/PUJOHvAfgcBuiDE/+Fwh0DZxZZyjrU+Cr+dbeowIdUaOO8BEDDJ0CUenJw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.883.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.879.0", + "@smithy/core": "^3.9.2", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } }, - "node_modules/@tsconfig/node10": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", - "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "dev": true, - "license": "MIT" + "node_modules/@aws-sdk/client-ses/node_modules/@aws-sdk/nested-clients": { + "version": "3.886.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.886.0.tgz", + "integrity": "sha512-CqeRdkNyJ7LlKLQtMTzK11WIiryEK8JbSL5LCia0B1Lp22OByDUiUSFZZ3FZq9poD5qHQI63pHkzAr5WkLGS5A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.883.0", + "@aws-sdk/middleware-host-header": "3.873.0", + "@aws-sdk/middleware-logger": "3.876.0", + "@aws-sdk/middleware-recursion-detection": "3.886.0", + "@aws-sdk/middleware-user-agent": "3.883.0", + "@aws-sdk/region-config-resolver": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.879.0", + "@aws-sdk/util-user-agent-browser": "3.873.0", + "@aws-sdk/util-user-agent-node": "3.883.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.9.2", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.21", + "@smithy/middleware-retry": "^4.1.22", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.5.2", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.29", + "@smithy/util-defaults-mode-node": "^4.0.29", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, - "license": "MIT" + "node_modules/@aws-sdk/client-ses/node_modules/@aws-sdk/token-providers": { + "version": "3.886.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.886.0.tgz", + "integrity": "sha512-dYS3apmGcldFglpAiAajcdDKtKjBw/NkG6nRYIC2q7+OZsxeyzunT1EUSxV4xphLoqiuhuCg/fTnBI3WVtb3IQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.883.0", + "@aws-sdk/nested-clients": "3.886.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, - "license": "MIT" + "node_modules/@aws-sdk/client-ses/node_modules/@aws-sdk/util-endpoints": { + "version": "3.879.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.879.0.tgz", + "integrity": "sha512-aVAJwGecYoEmbEFju3127TyJDF9qJsKDUUTRMDuS8tGn+QiWQFnfInmbt+el9GU1gEJupNTXV+E3e74y51fb7A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-endpoints": "^3.0.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-ses/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.883.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.883.0.tgz", + "integrity": "sha512-28cQZqC+wsKUHGpTBr+afoIdjS6IoEJkMqcZsmo2Ag8LzmTa6BUWQenFYB0/9BmDy4PZFPUn+uX+rJgWKB+jzA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.883.0", + "@aws-sdk/types": "3.862.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.873.0.tgz", + "integrity": "sha512-EmcrOgFODWe7IsLKFTeSXM9TlQ80/BO1MBISlr7w2ydnOaUYIiPGRRJnDpeIgMaNqT4Rr2cRN2RiMrbFO7gDdA==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.873.0", + "@aws-sdk/middleware-host-header": "3.873.0", + "@aws-sdk/middleware-logger": "3.873.0", + "@aws-sdk/middleware-recursion-detection": "3.873.0", + "@aws-sdk/middleware-user-agent": "3.873.0", + "@aws-sdk/region-config-resolver": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.873.0", + "@aws-sdk/util-user-agent-browser": "3.873.0", + "@aws-sdk/util-user-agent-node": "3.873.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.8.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-retry": "^4.1.19", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.26", + "@smithy/util-defaults-mode-node": "^4.0.26", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.873.0.tgz", + "integrity": "sha512-WrROjp8X1VvmnZ4TBzwM7RF+EB3wRaY9kQJLXw+Aes0/3zRjUXvGIlseobGJMqMEGnM0YekD2F87UaVfot1xeQ==", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@aws-sdk/xml-builder": "3.873.0", + "@smithy/core": "^3.8.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/property-provider": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/signature-v4": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-utf8": "^4.0.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.873.0.tgz", + "integrity": "sha512-FWj1yUs45VjCADv80JlGshAttUHBL2xtTAbJcAxkkJZzLRKVkdyrepFWhv/95MvDyzfbT6PgJiWMdW65l/8ooA==", + "dependencies": { + "@aws-sdk/core": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.873.0.tgz", + "integrity": "sha512-0sIokBlXIsndjZFUfr3Xui8W6kPC4DAeBGAXxGi9qbFZ9PWJjn1vt2COLikKH3q2snchk+AsznREZG8NW6ezSg==", + "dependencies": { + "@aws-sdk/core": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/property-provider": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/util-stream": "^4.2.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.873.0.tgz", + "integrity": "sha512-bQdGqh47Sk0+2S3C+N46aNQsZFzcHs7ndxYLARH/avYXf02Nl68p194eYFaAHJSQ1re5IbExU1+pbums7FJ9fA==", + "dependencies": { + "@aws-sdk/core": "3.873.0", + "@aws-sdk/credential-provider-env": "3.873.0", + "@aws-sdk/credential-provider-http": "3.873.0", + "@aws-sdk/credential-provider-process": "3.873.0", + "@aws-sdk/credential-provider-sso": "3.873.0", + "@aws-sdk/credential-provider-web-identity": "3.873.0", + "@aws-sdk/nested-clients": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.873.0.tgz", + "integrity": "sha512-+v/xBEB02k2ExnSDL8+1gD6UizY4Q/HaIJkNSkitFynRiiTQpVOSkCkA0iWxzksMeN8k1IHTE5gzeWpkEjNwbA==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.873.0", + "@aws-sdk/credential-provider-http": "3.873.0", + "@aws-sdk/credential-provider-ini": "3.873.0", + "@aws-sdk/credential-provider-process": "3.873.0", + "@aws-sdk/credential-provider-sso": "3.873.0", + "@aws-sdk/credential-provider-web-identity": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.873.0.tgz", + "integrity": "sha512-ycFv9WN+UJF7bK/ElBq1ugWA4NMbYS//1K55bPQZb2XUpAM2TWFlEjG7DIyOhLNTdl6+CbHlCdhlKQuDGgmm0A==", + "dependencies": { + "@aws-sdk/core": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.873.0.tgz", + "integrity": "sha512-SudkAOZmjEEYgUrqlUUjvrtbWJeI54/0Xo87KRxm4kfBtMqSx0TxbplNUAk8Gkg4XQNY0o7jpG8tK7r2Wc2+uw==", + "dependencies": { + "@aws-sdk/client-sso": "3.873.0", + "@aws-sdk/core": "3.873.0", + "@aws-sdk/token-providers": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.873.0.tgz", + "integrity": "sha512-Gw2H21+VkA6AgwKkBtTtlGZ45qgyRZPSKWs0kUwXVlmGOiPz61t/lBX0vG6I06ZIz2wqeTJ5OA1pWZLqw1j0JQ==", + "dependencies": { + "@aws-sdk/core": "3.873.0", + "@aws-sdk/nested-clients": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.873.0.tgz", + "integrity": "sha512-b4bvr0QdADeTUs+lPc9Z48kXzbKHXQKgTvxx/jXDgSW9tv4KmYPO1gIj6Z9dcrBkRWQuUtSW3Tu2S5n6pe+zeg==", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-arn-parser": "3.873.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-config-provider": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.873.0.tgz", + "integrity": "sha512-GIqoc8WgRcf/opBOZXFLmplJQKwOMjiOMmDz9gQkaJ8FiVJoAp8EGVmK2TOWZMQUYsavvHYsHaor5R2xwPoGVg==", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.873.0.tgz", + "integrity": "sha512-NNiy2Y876P5cgIhsDlHopbPZS3ugdfBW1va0WdpVBviwAs6KT4irPNPAOyF1/33N/niEDKx0fKQV7ROB70nNPA==", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-stream": "^4.2.4", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.873.0.tgz", + "integrity": "sha512-KZ/W1uruWtMOs7D5j3KquOxzCnV79KQW9MjJFZM/M0l6KI8J6V3718MXxFHsTjUE4fpdV6SeCNLV1lwGygsjJA==", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.873.0.tgz", + "integrity": "sha512-r+hIaORsW/8rq6wieDordXnA/eAu7xAPLue2InhoEX6ML7irP52BgiibHLpt9R0psiCzIHhju8qqKa4pJOrmiw==", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.873.0.tgz", + "integrity": "sha512-QhNZ8X7pW68kFez9QxUSN65Um0Feo18ZmHxszQZNUhKDsXew/EG9NPQE/HgYcekcon35zHxC4xs+FeNuPurP2g==", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.873.0.tgz", + "integrity": "sha512-OtgY8EXOzRdEWR//WfPkA/fXl0+WwE8hq0y9iw2caNyKPtca85dzrrZWnPqyBK/cpImosrpR1iKMYr41XshsCg==", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.873.0.tgz", + "integrity": "sha512-bOoWGH57ORK2yKOqJMmxBV4b3yMK8Pc0/K2A98MNPuQedXaxxwzRfsT2Qw+PpfYkiijrrNFqDYmQRGntxJ2h8A==", + "dependencies": { + "@aws-sdk/core": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-arn-parser": "3.873.0", + "@smithy/core": "^3.8.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/protocol-http": "^5.1.3", + "@smithy/signature-v4": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-stream": "^4.2.4", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.873.0.tgz", + "integrity": "sha512-AF55J94BoiuzN7g3hahy0dXTVZahVi8XxRBLgzNp6yQf0KTng+hb/V9UQZVYY1GZaDczvvvnqC54RGe9OZZ9zQ==", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.873.0.tgz", + "integrity": "sha512-gHqAMYpWkPhZLwqB3Yj83JKdL2Vsb64sryo8LN2UdpElpS+0fT4yjqSxKTfp7gkhN6TCIxF24HQgbPk5FMYJWw==", + "dependencies": { + "@aws-sdk/core": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.873.0", + "@smithy/core": "^3.8.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.873.0.tgz", + "integrity": "sha512-yg8JkRHuH/xO65rtmLOWcd9XQhxX1kAonp2CliXT44eA/23OBds6XoheY44eZeHfCTgutDLTYitvy3k9fQY6ZA==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.873.0", + "@aws-sdk/middleware-host-header": "3.873.0", + "@aws-sdk/middleware-logger": "3.873.0", + "@aws-sdk/middleware-recursion-detection": "3.873.0", + "@aws-sdk/middleware-user-agent": "3.873.0", + "@aws-sdk/region-config-resolver": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.873.0", + "@aws-sdk/util-user-agent-browser": "3.873.0", + "@aws-sdk/util-user-agent-node": "3.873.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.8.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-retry": "^4.1.19", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.26", + "@smithy/util-defaults-mode-node": "^4.0.26", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.873.0.tgz", + "integrity": "sha512-q9sPoef+BBG6PJnc4x60vK/bfVwvRWsPgcoQyIra057S/QGjq5VkjvNk6H8xedf6vnKlXNBwq9BaANBXnldUJg==", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/s3-request-presigner": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.873.0.tgz", + "integrity": "sha512-DiVlfCpdR7EaZSNPQwBB1jq8INWezKMWb3BUOWxrOcIcS3p2WpKbYl0H76D6TCHvQzXRVgKSSM6tHuWPoJtUHA==", + "dependencies": { + "@aws-sdk/signature-v4-multi-region": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-format-url": "3.873.0", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.873.0.tgz", + "integrity": "sha512-FQ5OIXw1rmDud7f/VO9y2Mg9rX1o4MnngRKUOD8mS9ALK4uxKrTczb4jA+uJLSLwTqMGs3bcB1RzbMW1zWTMwQ==", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/signature-v4": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.873.0.tgz", + "integrity": "sha512-BWOCeFeV/Ba8fVhtwUw/0Hz4wMm9fjXnMb4Z2a5he/jFlz5mt1/rr6IQ4MyKgzOaz24YrvqsJW2a0VUKOaYDvg==", + "dependencies": { + "@aws-sdk/core": "3.873.0", + "@aws-sdk/nested-clients": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.862.0.tgz", + "integrity": "sha512-Bei+RL0cDxxV+lW2UezLbCYYNeJm6Nzee0TpW0FfyTRBhH9C1XQh4+x+IClriXvgBnRquTMMYsmJfvx8iyLKrg==", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.873.0.tgz", + "integrity": "sha512-qag+VTqnJWDn8zTAXX4wiVioa0hZDQMtbZcGRERVnLar4/3/VIKBhxX2XibNQXFu1ufgcRn4YntT/XEPecFWcg==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.873.0.tgz", + "integrity": "sha512-YByHrhjxYdjKRf/RQygRK1uh0As1FIi9+jXTcIEX/rBgN8mUByczr2u4QXBzw7ZdbdcOBMOkPnLRjNOWW1MkFg==", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-endpoints": "^3.0.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@aws-sdk/util-format-url": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.873.0.tgz", + "integrity": "sha512-v//b9jFnhzTKKV3HFTw2MakdM22uBAs2lBov51BWmFXuFtSTdBLrR7zgfetQPE3PVkFai0cmtJQPdc3MX+T/cQ==", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/querystring-builder": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.873.0.tgz", + "integrity": "sha512-xcVhZF6svjM5Rj89T1WzkjQmrTF6dpR2UvIHPMTnSZoNe6CixejPZ6f0JJ2kAhO8H+dUHwNBlsUgOTIKiK/Syg==", + "dependencies": { + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.1.0", + "jest-util": "30.0.5", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.873.0.tgz", + "integrity": "sha512-AcRdbK6o19yehEcywI43blIBhOCSo6UgyWcuOJX5CFF8k39xm1ILCjQlRRjchLAxWrm0lU0Q7XV90RiMMFMZtA==", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.873.0.tgz", + "integrity": "sha512-9MivTP+q9Sis71UxuBaIY3h5jxH0vN3/ZWGxO8ADL19S2OIfknrYSAfzE5fpoKROVBu0bS4VifHOFq4PY1zsxw==", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.873.0.tgz", + "integrity": "sha512-kLO7k7cGJ6KaHiExSJWojZurF7SnGMDHXRuQunFnEoD0n1yB6Lqy/S/zHiQ7oJnBhPr9q0TW9qFkrsZb1Uc54w==", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.1.2", + "@jest/types": "30.0.5", + "@types/node": "*", + "jest-mock": "30.0.5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@googlemaps/google-maps-services-js": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@googlemaps/google-maps-services-js/-/google-maps-services-js-3.4.2.tgz", + "integrity": "sha512-QjxiJSt8woyPaaQIUUDNL1nRfCEXTiv8KfJSNq/YzKEUVXABT75GLG+Zo267UwOcq70n8OsZbaro5VrY962mJg==", + "license": "Apache-2.0", + "dependencies": { + "@googlemaps/url-signature": "^1.0.4", + "agentkeepalive": "^4.1.0", + "axios": "^1.5.1", + "query-string": "<8.x", + "retry-axios": "<3.x" + } + }, + "node_modules/@googlemaps/url-signature": { + "version": "1.0.40", + "resolved": "https://registry.npmjs.org/@googlemaps/url-signature/-/url-signature-1.0.40.tgz", + "integrity": "sha512-Gme3JxGZWQ4NVpATajSpS2/inQzhUxRvr/FK6IFpcC7AHOAmx8blI0y1/Qi2jqil+WoQ3TkEqq/MaKVtuV68RQ==", + "license": "Apache-2.0", + "dependencies": { + "crypto-js": "^4.2.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==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "expect": "30.1.2", + "jest-snapshot": "30.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@prisma/client": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.15.0.tgz", + "integrity": "sha512-wR2LXUbOH4cL/WToatI/Y2c7uzni76oNFND7+23ypLllBmIS8e3ZHhO+nud9iXSXKFt1SoM3fTZvHawg63emZw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.12.0.tgz", + "integrity": "sha512-HovZWzhWEMedHxmjefQBRZa40P81N7/+74khKFz9e1AFjakcIQdXgMWKgt20HaACzY+d1LRBC+L4tiz71t9fkg==", + "license": "Apache-2.0", + "dependencies": { + "jiti": "2.4.2" + } + }, + "node_modules/@prisma/debug": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.12.0.tgz", + "integrity": "sha512-plbz6z72orcqr0eeio7zgUrZj5EudZUpAeWkFTA/DDdXEj28YHDXuiakvR6S7sD6tZi+jiwQEJAPeV6J6m/tEQ==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.12.0.tgz", + "integrity": "sha512-4BRZZUaAuB4p0XhTauxelvFs7IllhPmNLvmla0bO1nkECs8n/o1pUvAVbQ/VOrZR5DnF4HED0PrGai+rIOVePA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.12.0", + "@prisma/engines-version": "6.12.0-15.8047c96bbd92db98a2abc7c9323ce77c02c89dbc", + "@prisma/fetch-engine": "6.12.0", + "@prisma/get-platform": "6.12.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "6.12.0-15.8047c96bbd92db98a2abc7c9323ce77c02c89dbc", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.12.0-15.8047c96bbd92db98a2abc7c9323ce77c02c89dbc.tgz", + "integrity": "sha512-70vhecxBJlRr06VfahDzk9ow4k1HIaSfVUT3X0/kZoHCMl9zbabut4gEXAyzJZxaCGi5igAA7SyyfBI//mmkbQ==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.12.0.tgz", + "integrity": "sha512-EamoiwrK46rpWaEbLX9aqKDPOd8IyLnZAkiYXFNuq0YsU0Z8K09/rH8S7feOWAVJ3xzeSgcEJtBlVDrajM9Sag==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.12.0", + "@prisma/engines-version": "6.12.0-15.8047c96bbd92db98a2abc7c9323ce77c02c89dbc", + "@prisma/get-platform": "6.12.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.12.0.tgz", + "integrity": "sha512-nRerTGhTlgyvcBlyWgt8OLNIV7QgJS2XYXMJD1hysorMCuLAjuDDuoxmVt7C2nLxbuxbWPp7OuFRHC23HqD9dA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.12.0" + } + }, + "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==", + "license": "BSD-3-Clause", + "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==", + "license": "BSD-3-Clause" + }, + "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==", + "license": "BSD-3-Clause" + }, + "node_modules/@smithy/abort-controller": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.1.1.tgz", + "integrity": "sha512-vkzula+IwRvPR6oKQhMYioM3A/oX/lFCZiwuxkQbRhqJS2S4YRY2k7k/SyR2jMf3607HLtbEwlRxi0ndXHMjRg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.0.0.tgz", + "integrity": "sha512-+sKqDBQqb036hh4NPaUiEkYFkTUGYzRsn3EuFhyfQfMy6oGHEUJDurLP9Ufb5dasr/XiAmPNMr6wa9afjQB+Gw==", + "dependencies": { + "@jest/environment": "30.1.2", + "@jest/expect": "30.1.2", + "@jest/types": "30.0.5", + "jest-mock": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.0.0.tgz", + "integrity": "sha512-R9wM2yPmfEMsUmlMlIgSzOyICs0x9uu7UTHoccMyt7BWw8shcGM8HqB355+BZCPBcySvbTYMs62EgEQkNxz2ig==", + "dependencies": { + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.2.1.tgz", + "integrity": "sha512-FXil8q4QN7mgKwU2hCLm0ltab8NyY/1RiqEf25Jnf6WLS3wmb11zGAoLETqg1nur2Aoibun4w4MjeN9CMJ4G6A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.2.1", + "@smithy/types": "^4.5.0", + "@smithy/util-config-provider": "^4.1.0", + "@smithy/util-middleware": "^4.1.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.11.0.tgz", + "integrity": "sha512-Abs5rdP1o8/OINtE49wwNeWuynCu0kme1r4RI3VXVrHr4odVDG7h7mTnw1WXXfN5Il+c25QOnrdL2y56USfxkA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.1.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-body-length-browser": "^4.1.0", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-stream": "^4.3.1", + "@smithy/util-utf8": "^4.1.0", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.1.1.tgz", + "integrity": "sha512-1WdBfM9DwA59pnpIizxnUvBf/de18p4GP+6zP2AqrlFzoW3ERpZaT4QueBR0nS9deDMaQRkBlngpVlnkuuTisQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.2.1", + "@smithy/property-provider": "^4.1.1", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.0.5.tgz", + "integrity": "sha512-miEUN+nz2UTNoRYRhRqVTJCx7jMeILdAurStT2XoS+mhokkmz1xAPp95DFW9Gxt4iF2VBqpeF9HbTQ3kY1viOA==", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.3.2", + "@smithy/util-hex-encoding": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.0.5.tgz", + "integrity": "sha512-LCUQUVTbM6HFKzImYlSB9w4xafZmpdmZsOh9rIl7riPC3osCgGFVP+wwvYVw6pXda9PPT9TcEZxaq3XE81EdJQ==", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.1.3.tgz", + "integrity": "sha512-yTTzw2jZjn/MbHu1pURbHdpjGbCuMHWncNBpJnQAPxOVnFUAbSIUSwafiphVDjNV93TdBJWmeVAds7yl5QCkcA==", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.0.5.tgz", + "integrity": "sha512-lGS10urI4CNzz6YlTe5EYG0YOpsSp3ra8MXyco4aqSkQDuyZPIw2hcaxDU82OUVtK7UY9hrSvgWtpsW5D4rb4g==", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.0.5.tgz", + "integrity": "sha512-JFnmu4SU36YYw3DIBVao3FsJh4Uw65vVDIqlWT4LzR6gXA0F3KP0IXFKKJrhaVzCBhAuMsrUUaT5I+/4ZhF7aw==", + "dependencies": { + "@smithy/eventstream-codec": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.2.1.tgz", + "integrity": "sha512-5/3wxKNtV3wO/hk1is+CZUhL8a1yy/U+9u9LKQ9kZTkMsHaQjJhc3stFfiujtMnkITjzWfndGA2f7g9Uh9vKng==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.2.1", + "@smithy/querystring-builder": "^4.1.1", + "@smithy/types": "^4.5.0", + "@smithy/util-base64": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-blob-browser": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.0.5.tgz", + "integrity": "sha512-F7MmCd3FH/Q2edhcKd+qulWkwfChHbc9nhguBlVjSUE6hVHhec3q6uPQ+0u69S6ppvLtR3eStfCuEKMXBXhvvA==", + "dependencies": { + "@smithy/chunked-blob-reader": "^5.0.0", + "@smithy/chunked-blob-reader-native": "^4.0.0", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.5.tgz", + "integrity": "sha512-cv1HHkKhpyRb6ahD8Vcfb2Hgz67vNIXEp2vnhzfxLFGRukLCNEA5QdsorbUEzXma1Rco0u3rx5VTqbM06GcZqQ==", + "dependencies": { + "@smithy/types": "^4.3.2", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-stream-node": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.0.5.tgz", + "integrity": "sha512-IJuDS3+VfWB67UC0GU0uYBG/TA30w+PlOaSo0GPm9UHS88A6rCP6uZxNjNYiyRtOcjv7TXn/60cW8ox1yuZsLg==", + "dependencies": { + "@smithy/types": "^4.3.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.5.tgz", + "integrity": "sha512-IVnb78Qtf7EJpoEVo7qJ8BEXQwgC4n3igeJNNKEj/MLYtapnx8A67Zt/J3RXAj2xSO1910zk0LdFiygSemuLow==", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@smithy/md5-js": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.0.5.tgz", + "integrity": "sha512-8n2XCwdUbGr8W/XhMTaxILkVlw2QebkVTn5tm3HOcbPbOpWg89zr6dPXsH8xbeTsbTXlJvlJNTQsKAIoqQGbdA==", + "dependencies": { + "@smithy/types": "^4.3.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.5.tgz", + "integrity": "sha512-l1jlNZoYzoCC7p0zCtBDE5OBXZ95yMKlRlftooE5jPWQn4YBPLgsp+oeHp7iMHaTGoUdFqmHOPa8c9G3gBsRpQ==", + "dependencies": { + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.2.1.tgz", + "integrity": "sha512-fUTMmQvQQZakXOuKizfu7fBLDpwvWZjfH6zUK2OLsoNZRZGbNUdNSdLJHpwk1vS208jtDjpUIskh+JoA8zMzZg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.11.0", + "@smithy/middleware-serde": "^4.1.1", + "@smithy/node-config-provider": "^4.2.1", + "@smithy/shared-ini-file-loader": "^4.1.1", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "@smithy/util-middleware": "^4.1.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.2.1.tgz", + "integrity": "sha512-JzfvjwSJXWRl7LkLgIRTUTd2Wj639yr3sQGpViGNEOjtb0AkAuYqRAHs+jSOI/LPC0ZTjmFVVtfrCICMuebexw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.2.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/service-error-classification": "^4.1.1", + "@smithy/smithy-client": "^4.6.1", + "@smithy/types": "^4.5.0", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-retry": "^4.1.1", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.1.1.tgz", + "integrity": "sha512-lh48uQdbCoj619kRouev5XbWhCwRKLmphAif16c4J6JgJ4uXjub1PI6RL38d3BLliUvSso6klyB/LTNpWSNIyg==", + "license": "Apache-2.0", + "dependencies": { + "@jest/test-result": "30.1.3", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.1.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.1.2.tgz", + "integrity": "sha512-UYYFGifSgfjujf1Cbd3iU/IQoSd6uwsj8XHj5DSDf5ERDcWMdJOPTkHWXj4U+Z/uMagyOQZ6Vne8C4nRIrCxqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.0.5", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.0", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.1.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.0.5", + "micromatch": "^4.0.8", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.2.1.tgz", + "integrity": "sha512-AIA0BJZq2h295J5NeCTKhg1WwtdTA/GqBCaVjk30bDgMHwniUETyh5cP9IiE9VrId7Kt8hS7zvREVMTv1VfA6g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.1.1", + "@smithy/shared-ini-file-loader": "^4.1.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.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, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/gen-mapping/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, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "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, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping/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, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.1.1.tgz", + "integrity": "sha512-Iam75b/JNXyDE41UvrlM6n8DNOa/r1ylFyvgruTUx7h2Uk7vDNV9AAwP1vfL1fOL8ls0xArwEGVcGZVd7IO/Cw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.1.1.tgz", + "integrity": "sha512-YkpikhIqGc4sfXeIbzSj10t2bJI/sSoP5qxLue6zG+tEE3ngOBSm8sO3+djacYvS/R5DfpxN/L9CyZsvwjWOAQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.3.tgz", + "integrity": "sha512-mARDSXSEgllNzMw6N+mC+r1AQlEBO3meEAkR/UlfAgnMzJUB3goRBWgip1EAMG99wh36MDqzo86SfIX5Y+VEaw==", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.6.1.tgz", + "integrity": "sha512-WolVLDb9UTPMEPPOncrCt6JmAMCSC/V2y5gst2STWJ5r7+8iNac+EFYQnmvDCYMfOLcilOSEpm5yXZXwbLak1Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.11.0", + "@smithy/middleware-endpoint": "^4.2.1", + "@smithy/middleware-stack": "^4.1.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "@smithy/util-stream": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", + "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@prisma/client": { + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.16.2.tgz", + "integrity": "sha512-E00PxBcalMfYO/TWnXobBVUai6eW/g5OsifWQsQDzJYm7yaY+IRLo7ZLsaefi0QkTpxfuhFcQ/w180i6kX3iJw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.1.1.tgz", + "integrity": "sha512-hA1AKIHFUMa9Tl6q6y8p0pJ9aWHCCG8s57flmIyLE0W7HcJeYrYtnqXDcGnftvXEhdQnSexyegXnzzTGk8bKLA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.1.1", + "@smithy/smithy-client": "^4.6.1", + "@smithy/types": "^4.5.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.1.1.tgz", + "integrity": "sha512-RGSpmoBrA+5D2WjwtK7tto6Pc2wO9KSXKLpLONhFZ8VyuCbqlLdiDAfuDTNY9AJe4JoE+Cx806cpTQQoQ71zPQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.2.1", + "@smithy/credential-provider-imds": "^4.1.1", + "@smithy/node-config-provider": "^4.2.1", + "@smithy/property-provider": "^4.1.1", + "@smithy/smithy-client": "^4.6.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.7.tgz", + "integrity": "sha512-klGBP+RpBp6V5JbrY2C/VKnHXn3d5V2YrifZbmMY8os7M6m8wdYFoO6w/fe5VkP+YVwrEktW3IWYaSQVNZJ8oQ==", + "dependencies": { + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@prisma/config": { + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.16.2.tgz", + "integrity": "sha512-mKXSUrcqXj0LXWPmJsK2s3p9PN+aoAbyMx7m5E1v1FufofR1ZpPoIArjjzOIm+bJRLLvYftoNYLx1tbHgF9/yg==", + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.16.12", + "empathic": "2.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.1.1.tgz", + "integrity": "sha512-CGmZ72mL29VMfESz7S6dekqzCh8ZISj3B+w0g1hZFXaOjGTVaSqfAEFAq8EGp8fUL+Q2l8aqNmt8U1tglTikeg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.1.1.tgz", + "integrity": "sha512-jGeybqEZ/LIordPLMh5bnmnoIgsqnp4IEimmUp5c5voZ8yx+5kAlN5+juyr7p+f7AtZTgvhmInQk4Q0UVbrZ0Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.1.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.3.1.tgz", + "integrity": "sha512-khKkW/Jqkgh6caxMWbMuox9+YfGlsk9OnHOYCGVEdYQb/XVzcORXHLYUubHmmda0pubEDncofUrPNniS9d+uAA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.16.2", + "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", + "@prisma/get-platform": "6.16.2" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.16.2.tgz", + "integrity": "sha512-U/P36Uke5wS7r1+omtAgJpEB94tlT4SdlgaeTc6HVTTT93pXj7zZ+B/cZnmnvjcNPfWddgoDx8RLjmQwqGDYyA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.16.2" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.0.7.tgz", + "integrity": "sha512-mYqtQXPmrwvUljaHyGxYUIIRI3qjBTEb/f5QFi3A6VlxhpmZd5mWXn9W+qUkf2pVE1Hv3SqxefiZOPGdxmO64A==", + "dependencies": { + "@smithy/abort-controller": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aws-sdk": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/@types/aws-sdk/-/aws-sdk-2.7.4.tgz", + "integrity": "sha512-BdGaQDSow2hYmHbn7RV/Lg9rvh/JBD6gFRKAeCh3eqjc2eAjaz5m+cjuX1lpaWOisMeb0ep8sZBhtOLHHZ8qAA==", + "deprecated": "This is a stub types definition. aws-sdk provides its own type definitions, so you do not need this installed.", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", + "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz", + "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "license": "MIT" + }, + "node_modules/@types/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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/joi": { + "version": "17.2.2", + "resolved": "https://registry.npmjs.org/@types/joi/-/joi-17.2.2.tgz", + "integrity": "sha512-vPvPwxn0Y4pQyqkEcMCJYxXCMYcrHqdfFX4SpF4zcqYioYexmDyxtM3OK+m/ZwGBS8/dooJ0il9qCwAdd6KFtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "joi": "*" + } + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.0.tgz", + "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.10.0" + } + }, + "node_modules/@types/pg": { + "version": "8.15.5", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.5.tgz", + "integrity": "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==" + }, + "node_modules/accepts": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/amqplib": { + "version": "0.10.9", + "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.9.tgz", + "integrity": "sha512-jwSftI4QjS3mizvnSnOrPGYiUnm1vI2OP1iXeOUz5pb74Ua0nbf6nPyyTzuiCLEE3fMpaJORXh2K/TQ08H5xGA==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "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==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/babel-jest": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.1.2.tgz", + "integrity": "sha512-IQCus1rt9kaSh7PQxLYRY5NmkNrNlU2TpabzwV7T2jljnpdHOcmnYYv8QmE04Li4S3a2Lj8/yXyET5pBarPr6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.1.2", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.0", + "babel-preset-jest": "30.0.1", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0" + } + }, + "node_modules/aws-sdk": { + "version": "2.1692.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1692.0.tgz", + "integrity": "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw==", + "hasInstallScript": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/axios": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.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==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.0.1", + "babel-preset-current-node-syntax": "^1.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.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==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.5.tgz", + "integrity": "sha512-TiU4qUT9jdCuh4aVOG7H1QozyeI2sZRqoRPdqBIaslfNt4WUSanRBueAwl2x5jt4rXBMim3lIN2x6yT8PDi24Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bcryptjs": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", + "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "2.2.0", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/bowser": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.0.tgz", + "integrity": "sha512-HcOcTudTeEWgbHh0Y1Tyb6fdeR71m4b/QACf0D4KswGTsNeIJQmg38mRENZPAYPZvGFN3fk3604XbQEPdxXdKg==" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.26.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", + "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", + "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" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.3", + "caniuse-lite": "^1.0.30001741", + "electron-to-chromium": "^1.5.218", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "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, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/c12/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/c12/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/c12/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001743", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001743.tgz", + "integrity": "sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==", + "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" + } + ], + "license": "CC-BY-4.0" + }, + "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, + "license": "MIT", + "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/chalk/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, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chalk/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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/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, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "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==", + "license": "MIT", + "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/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/confbox": { + "version": "0.2.2", + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "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, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": 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, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", + "integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/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, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/engine.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "license": "MIT", + "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==", + "license": "MIT", + "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/escape-html": { + "version": "1.0.3", + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "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/execa/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==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.1.2.tgz", + "integrity": "sha512-xvHszRavo28ejws8FpemjhwswGj4w/BetHIL8cU49u4sGyXDw2+p3YbeDbj6xzlxi6kWTjIRSTJ+9sNXPnF0Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.1.2", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.1.2", + "jest-message-util": "30.1.0", + "jest-mock": "30.0.5", + "jest-util": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/express": { + "version": "5.1.0", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.0.1.tgz", + "integrity": "sha512-aZVCnybn7TVmxO4BtlmnvX+nuz8qHW124KKJ8dumsBsmv5ZLxE0pYu7S2nwyRBGHHCAzdmnGyrc5U/rksSPO7Q==", + "dependencies": { + "ip-address": "10.0.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.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==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "license": "MIT", + "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, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-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, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "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, "license": "MIT" }, - "node_modules/@types/node": { - "version": "24.1.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", - "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", - "dev": true, + "node_modules/http-errors": { + "version": "2.0.0", "license": "MIT", - "peer": true, "dependencies": { - "undici-types": "~7.8.0" + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" } }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", "license": "MIT", "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">=0.10.0" } }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "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==", + "license": "ISC" + }, + "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, "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, "bin": { - "acorn": "bin/acorn" + "import-local-fixture": "fixtures/cli.js" }, "engines": { - "node": ">=0.4.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, "engines": { - "node": ">=0.4.0" + "node": ">=0.8.19" } }, - "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==", + "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.", + "dev": true, "license": "ISC", "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", "engines": { - "node": ">= 8" + "node": ">= 12" } }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "license": "MIT" + "node_modules/ipaddr.js": { + "version": "1.9.1", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } }, - "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/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, "license": "MIT" }, - "node_modules/bcrypt": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", - "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", - "hasInstallScript": 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==", "license": "MIT", "dependencies": { - "node-addon-api": "^8.3.0", - "node-gyp-build": "^4.8.4" + "binary-extensions": "^2.0.0" }, "engines": { - "node": ">= 18" + "node": ">=8" } }, - "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==", + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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, "license": "MIT", "engines": { "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "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, + "license": "MIT", + "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==", "license": "MIT", "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "is-extglob": "^2.1.1" }, "engines": { - "node": ">=18" + "node": ">=0.10.0" } }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "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==", "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "engines": { + "node": ">=0.12.0" } }, - "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==", + "node_modules/is-promise": { + "version": "4.0.0", + "license": "MIT" + }, + "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==", + "dev": true, "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", + "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, + "license": "BSD-3-Clause", "engines": { - "node": ">= 0.8" + "node": ">=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==", - "license": "MIT", + "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, + "license": "BSD-3-Clause", "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" + "@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": ">= 0.4" + "node": ">=10" } }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", + "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, + "license": "BSD-3-Clause", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "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==", + "node_modules/istanbul-lib-report/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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/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, "license": "MIT", "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" + "has-flag": "^4.0.0" }, "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "node": ">=8" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT" + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } }, - "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "node_modules/istanbul-lib-source-maps/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, "license": "MIT", "dependencies": { - "safe-buffer": "5.2.1" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "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, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">=8" } }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", + "node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, "engines": { - "node": ">= 0.6" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "node_modules/jest": { + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.1.3.tgz", + "integrity": "sha512-Ry+p2+NLk6u8Agh5yVqELfUJvRfV51hhVBRIB5yZPY7mU0DGBmOuFG5GebZbMbm86cdQNK0fhJuDX8/1YorISQ==", + "dev": true, "license": "MIT", + "dependencies": { + "@jest/core": "30.1.3", + "@jest/types": "30.0.5", + "import-local": "^3.2.0", + "jest-cli": "30.1.3" + }, + "bin": { + "jest": "bin/jest.js" + }, "engines": { - "node": ">= 0.6" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "node_modules/jest-changed-files": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.0.5.tgz", + "integrity": "sha512-bGl2Ntdx0eAwXuGpdLdVYVr5YQHnSZlQ0y9HVDu565lCUAe9sj6JOtBbMmBBikGIegne9piDDIOeiLVoqTkz4A==", + "dev": true, "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.0.5", + "p-limit": "^3.1.0" + }, "engines": { - "node": ">=6.6.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "node_modules/jest-circus": { + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.1.3.tgz", + "integrity": "sha512-Yf3dnhRON2GJT4RYzM89t/EXIWNxKTpWTL9BfF3+geFetWP4XSvJjiU1vrWplOiUkmq8cHLiwuhz+XuUp9DscA==", + "dev": true, "license": "MIT", "dependencies": { - "object-assign": "^4", - "vary": "^1" + "@jest/environment": "30.1.2", + "@jest/expect": "30.1.2", + "@jest/test-result": "30.1.3", + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.1.0", + "jest-matcher-utils": "30.1.2", + "jest-message-util": "30.1.0", + "jest-runtime": "30.1.3", + "jest-snapshot": "30.1.2", + "jest-util": "30.0.5", + "p-limit": "^3.1.0", + "pretty-format": "30.0.5", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" }, "engines": { - "node": ">= 0.10" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "node_modules/jest-circus/node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], "license": "MIT" }, - "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", "license": "MIT", "dependencies": { - "ms": "^2.1.3" + "@jest/core": "30.1.3", + "@jest/test-result": "30.1.3", + "@jest/types": "30.0.5", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.1.3", + "jest-util": "30.0.5", + "jest-validate": "30.1.0", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" }, "engines": { - "node": ">=6.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "peerDependenciesMeta": { - "supports-color": { + "node-notifier": { "optional": true } } }, - "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==", + "node_modules/jest-config": { + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.1.3.tgz", + "integrity": "sha512-M/f7gqdQEPgZNA181Myz+GXCe8jXcJsGjCMXUzRj22FIXsZOyHNte84e0exntOvdPaeh9tA0w+B8qlP2fAezfw==", + "dev": true, "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.1.3", + "@jest/types": "30.0.5", + "babel-jest": "30.1.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-circus": "30.1.3", + "jest-docblock": "30.0.1", + "jest-environment-node": "30.1.2", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.1.3", + "jest-runner": "30.1.3", + "jest-util": "30.0.5", + "jest-validate": "30.1.0", + "micromatch": "^4.0.8", + "parse-json": "^5.2.0", + "pretty-format": "30.0.5", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, "engines": { - "node": ">= 0.8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } } }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "node_modules/jest-config/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/jest-config/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-config/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest-config/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/jest-config/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, "engines": { - "node": ">=0.3.1" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/dotenv": { - "version": "17.2.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", - "integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==", - "license": "BSD-2-Clause", + "node_modules/jest-config/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, "engines": { - "node": ">=12" + "node": ">=16 || 14 >=14.18" }, "funding": { - "url": "https://dotenvx.com" + "url": "https://github.com/sponsors/isaacs" } }, - "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==", + "node_modules/jest-diff": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.1.2.tgz", + "integrity": "sha512-4+prq+9J61mOVXCa4Qp8ZjavdxzrWQXrI80GNxP8f4tkI2syPuPrJgdRPZRrfUTRvIoUwcmNLbqEJy9W800+NQ==", + "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.0.5" }, "engines": { - "node": ">= 0.4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "license": "Apache-2.0", + "node_modules/jest-docblock": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.0.1.tgz", + "integrity": "sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA==", + "dev": true, + "license": "MIT", "dependencies": { - "safe-buffer": "^5.0.1" + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "node_modules/jest-each": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.1.0.tgz", + "integrity": "sha512-A+9FKzxPluqogNahpCv04UJvcZ9B3HamqpDNWNKDjtxVRYB8xbZLFuCr8JAJFpNp83CA0anGQFlpQna9Me+/tQ==", + "dev": true, "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.0.5", + "chalk": "^4.1.2", + "jest-util": "30.0.5", + "pretty-format": "30.0.5" + }, "engines": { - "node": ">= 0.8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "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==", + "node_modules/jest-environment-node": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.1.2.tgz", + "integrity": "sha512-w8qBiXtqGWJ9xpJIA98M0EIoq079GOQRQUyse5qg1plShUCQ0Ek1VTTcczqKrn3f24TFAgFtT+4q3aOXvjbsuA==", + "dev": true, "license": "MIT", + "dependencies": { + "@jest/environment": "30.1.2", + "@jest/fake-timers": "30.1.2", + "@jest/types": "30.0.5", + "@types/node": "*", + "jest-mock": "30.0.5", + "jest-util": "30.0.5", + "jest-validate": "30.1.0" + }, "engines": { - "node": ">= 0.4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "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==", + "node_modules/jest-haste-map": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.1.0.tgz", + "integrity": "sha512-JLeM84kNjpRkggcGpQLsV7B8W4LNUWz7oDNVnY1Vjj22b5/fAb3kk3htiD+4Na8bmJmjJR7rBtS2Rmq/NEcADg==", + "dev": true, "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.0.5", + "jest-worker": "30.1.0", + "micromatch": "^4.0.8", + "walker": "^1.0.8" + }, "engines": { - "node": ">= 0.4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" } }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "node_modules/jest-leak-detector": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.1.0.tgz", + "integrity": "sha512-AoFvJzwxK+4KohH60vRuHaqXfWmeBATFZpzpmzNmYTtmRMiyGPVhkXpBqxUQunw+dQB48bDf4NpUs6ivVbRv1g==", + "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0" + "@jest/get-type": "30.1.0", + "pretty-format": "30.0.5" }, "engines": { - "node": ">= 0.4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" + "node_modules/jest-matcher-utils": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.1.2.tgz", + "integrity": "sha512-7ai16hy4rSbDjvPTuUhuV8nyPBd6EX34HkBsBcBX2lENCuAQ0qKCPb/+lt8OSWUa9WWmGYLy41PrEzkwRwoGZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.1.2", + "pretty-format": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "node_modules/jest-message-util": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.1.0.tgz", + "integrity": "sha512-HizKDGG98cYkWmaLUHChq4iN+oCENohQLb7Z5guBPumYs+/etonmNFlg1Ps6yN9LTPyZn+M+b/9BbnHx3WTMDg==", + "dev": true, "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.0.5", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.0.5", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, "engines": { - "node": ">= 0.6" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "node_modules/jest-mock": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.5.tgz", + "integrity": "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==", + "dev": true, "license": "MIT", "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.0", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" + "@jest/types": "30.0.5", + "@types/node": "*", + "jest-util": "30.0.5" }, "engines": { - "node": ">= 18" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.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, + "license": "MIT", + "engines": { + "node": ">=6" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } } }, - "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==", + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.1.3.tgz", + "integrity": "sha512-DI4PtTqzw9GwELFS41sdMK32Ajp3XZQ8iygeDMWkxlRhm7uUTOFSZFVZABFuxr0jvspn8MAYy54NxZCsuCTSOw==", + "dev": true, "license": "MIT", "dependencies": { - "to-regex-range": "^5.0.1" + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.1.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.0.5", + "jest-validate": "30.1.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" }, "engines": { - "node": ">=8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "node_modules/jest-resolve-dependencies": { + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.1.3.tgz", + "integrity": "sha512-DNfq3WGmuRyHRHfEet+Zm3QOmVFtIarUOQHHryKPc0YL9ROfgWZxl4+aZq/VAzok2SS3gZdniP+dO4zgo59hBg==", + "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.1.2" }, "engines": { - "node": ">= 0.8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "node_modules/jest-runner": { + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.1.3.tgz", + "integrity": "sha512-dd1ORcxQraW44Uz029TtXj85W11yvLpDuIzNOlofrC8GN+SgDlgY4BvyxJiVeuabA1t6idjNbX59jLd2oplOGQ==", + "dev": true, "license": "MIT", + "dependencies": { + "@jest/console": "30.1.2", + "@jest/environment": "30.1.2", + "@jest/test-result": "30.1.3", + "@jest/transform": "30.1.2", + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.0.1", + "jest-environment-node": "30.1.2", + "jest-haste-map": "30.1.0", + "jest-leak-detector": "30.1.0", + "jest-message-util": "30.1.0", + "jest-resolve": "30.1.3", + "jest-runtime": "30.1.3", + "jest-util": "30.0.5", + "jest-watcher": "30.1.3", + "jest-worker": "30.1.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, "engines": { - "node": ">= 0.6" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "node_modules/jest-runtime": { + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.1.3.tgz", + "integrity": "sha512-WS8xgjuNSphdIGnleQcJ3AKE4tBKOVP+tKhCD0u+Tb2sBmsU8DxfbBpZX7//+XOz81zVs4eFpJQwBNji2Y07DA==", + "dev": true, "license": "MIT", + "dependencies": { + "@jest/environment": "30.1.2", + "@jest/fake-timers": "30.1.2", + "@jest/globals": "30.1.2", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.1.3", + "@jest/transform": "30.1.2", + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.1.0", + "jest-message-util": "30.1.0", + "jest-mock": "30.0.5", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.1.3", + "jest-snapshot": "30.1.2", + "jest-util": "30.0.5", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, "engines": { - "node": ">= 0.8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, + "node_modules/jest-runtime/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "dependencies": { + "balanced-match": "^1.0.0" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", + "node_modules/jest-runtime/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", + "node_modules/jest-runtime/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest-runtime/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/jest-runtime/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">=16 || 14 >=14.17" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/isaacs" } }, - "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==", + "node_modules/jest-runtime/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-snapshot": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.1.2.tgz", + "integrity": "sha512-4q4+6+1c8B6Cy5pGgFvjDy/Pa6VYRiGu0yQafKkJ9u6wQx4G5PqI2QR6nxTl43yy7IWsINwz6oT4o6tD12a8Dg==", + "dev": true, "license": "MIT", "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.1.2", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.1.2", + "@jest/transform": "30.1.2", + "@jest/types": "30.0.5", + "babel-preset-current-node-syntax": "^1.1.0", + "chalk": "^4.1.2", + "expect": "30.1.2", + "graceful-fs": "^4.2.11", + "jest-diff": "30.1.2", + "jest-matcher-utils": "30.1.2", + "jest-message-util": "30.1.0", + "jest-util": "30.0.5", + "pretty-format": "30.0.5", + "semver": "^7.7.2", + "synckit": "^0.11.8" }, "engines": { - "node": ">= 0.4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "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==", - "license": "ISC", + "node_modules/jest-util": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz", + "integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==", + "dev": true, + "license": "MIT", "dependencies": { - "is-glob": "^4.0.1" + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" }, "engines": { - "node": ">= 6" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "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==", + "node_modules/jest-validate": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.1.0.tgz", + "integrity": "sha512-7P3ZlCFW/vhfQ8pE7zW6Oi4EzvuB4sgR72Q1INfW9m0FGo0GADYlPwIkf4CyPq7wq85g+kPMtPOHNAdWHeBOaA==", + "dev": true, "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.0.5", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.0.5" + }, "engines": { - "node": ">=4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "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==", + "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, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "node_modules/jest-watcher": { + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.1.3.tgz", + "integrity": "sha512-6jQUZCP1BTL2gvG9E4YF06Ytq4yMb4If6YoQGRR6PpjtqOXSP3sKe2kqwB6SQ+H9DezOfZaSLnmka1NtGm3fCQ==", + "dev": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.2" + "@jest/test-result": "30.1.3", + "@jest/types": "30.0.5", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.0.5", + "string-length": "^4.0.2" }, "engines": { - "node": ">= 0.4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "node_modules/jest-worker": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.1.0.tgz", + "integrity": "sha512-uvWcSjlwAAgIu133Tt77A05H7RIk3Ho8tZL50bQM2AkvLdluw9NG48lRCl3Dt+MOH719n/0nnb5YxUwcuJiKRA==", + "dev": true, "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.0.5", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" }, "engines": { - "node": ">= 0.8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "node_modules/jest-worker/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, "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">=8" } }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "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, "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "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==", - "license": "ISC" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "node_modules/jiti": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", "license": "MIT", - "engines": { - "node": ">= 0.10" + "bin": { + "jiti": "lib/jiti-cli.mjs" } }, - "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==", - "license": "MIT", + "node_modules/joi": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.0.1.tgz", + "integrity": "sha512-IiQpRyypSnLisQf3PwuN2eIHAsAIGZIrLZkd4zdvIar2bDyhM91ubRjy8a3eYablXsh9BeI/c7dmPYHca5qtoA==", + "license": "BSD-3-Clause", "dependencies": { - "binary-extensions": "^2.0.0" + "@hapi/address": "^5.1.1", + "@hapi/formula": "^3.0.2", + "@hapi/hoek": "^11.0.7", + "@hapi/pinpoint": "^2.0.1", + "@hapi/tlds": "^1.1.1", + "@hapi/topo": "^6.0.2", + "@standard-schema/spec": "^1.0.0" }, "engines": { - "node": ">=8" + "node": ">= 20" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.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, + "license": "MIT" }, - "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==", + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, "license": "MIT", "dependencies": { - "is-extglob": "^2.1.1" + "argparse": "^1.0.7", + "esprima": "^4.0.0" }, - "engines": { - "node": ">=0.10.0" + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "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==", + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, "engines": { - "node": ">=0.12.0" + "node": ">=6" } }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "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, "license": "MIT" }, - "node_modules/jiti": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "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, "license": "MIT", "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/joi": { - "version": "17.13.3", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", - "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", - "license": "BSD-3-Clause", - "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" + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" } }, "node_modules/jsonwebtoken": { "version": "9.0.2", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", - "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", "license": "MIT", "dependencies": { "jws": "^3.2.2", @@ -981,10 +5323,18 @@ "npm": ">=6" } }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.7.2", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jwa": { "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", - "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", "license": "MIT", "dependencies": { "buffer-equal-constant-time": "^1.0.1", @@ -994,67 +5344,120 @@ }, "node_modules/jws": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", "license": "MIT", "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/lodash.includes": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", "license": "MIT" }, "node_modules/lodash.isboolean": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", "license": "MIT" }, "node_modules/lodash.isinteger": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", "license": "MIT" }, "node_modules/lodash.isnumber": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", "license": "MIT" }, "node_modules/lodash.isplainobject": { "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", "license": "MIT" }, "node_modules/lodash.isstring": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, "license": "MIT" }, "node_modules/lodash.once": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, + "node_modules/lru-cache": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.1.tgz", + "integrity": "sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "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, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/make-error": { "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true, "license": "ISC" }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "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==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -1062,8 +5465,6 @@ }, "node_modules/media-typer": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -1071,8 +5472,6 @@ }, "node_modules/merge-descriptors": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "license": "MIT", "engines": { "node": ">=18" @@ -1081,10 +5480,52 @@ "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, + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "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, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/mime-db": { "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -1092,8 +5533,6 @@ }, "node_modules/mime-types": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "license": "MIT", "dependencies": { "mime-db": "^1.54.0" @@ -1114,30 +5553,72 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/ms": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "node_modules/napi-postinstall": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz", + "integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==", + "dev": true, "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, "engines": { - "node": ">= 0.6" + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" } }, - "node_modules/node-addon-api": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", - "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "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, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", "license": "MIT", "engines": { - "node": "^18 || ^20 || >= 21" + "node": ">= 0.6" } }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, "node_modules/node-gyp-build": { "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", @@ -1177,6 +5658,51 @@ "url": "https://opencollective.com/nodemon" } }, + "node_modules/nodemon/node_modules/brace-expansion": { + "version": "1.1.12", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/minimatch": { + "version": "3.1.2", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/nodemon/node_modules/semver": { + "version": "7.7.2", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -1188,8 +5714,6 @@ }, "node_modules/object-assign": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -1197,8 +5721,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==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -1209,8 +5731,6 @@ }, "node_modules/on-finished": { "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -1221,8 +5741,6 @@ }, "node_modules/once": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "license": "ISC", "dependencies": { "wrappy": "1" @@ -1233,8 +5751,14 @@ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, "engines": { - "node": ">= 0.8" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/path-to-regexp": { @@ -1246,6 +5770,164 @@ "node": ">=16" } }, + "node_modules/pathe": { + "version": "2.0.3", + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/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, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "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, + "license": "MIT", + "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==", + "license": "MIT", + "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, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, + "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, + "license": "ISC" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -1258,6 +5940,67 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 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, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/pretty-format": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", + "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/prisma": { "version": "6.12.0", "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.12.0.tgz", @@ -1265,8 +6008,8 @@ "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/config": "6.12.0", - "@prisma/engines": "6.12.0" + "@prisma/config": "6.16.2", + "@prisma/engines": "6.16.2" }, "bin": { "prisma": "build/index.js" @@ -1285,8 +6028,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==", "license": "MIT", "dependencies": { "forwarded": "0.2.0", @@ -1296,16 +6037,36 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "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==", "license": "MIT" }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/qs": { "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -1317,10 +6078,41 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/query-string": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "license": "MIT", + "dependencies": { + "decode-uri-component": "^0.2.2", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -1334,13 +6126,46 @@ "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", - "iconv-lite": "0.6.3", + "iconv-lite": "0.7.0", "unpipe": "1.0.0" }, "engines": { "node": ">= 0.8" } }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "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, + "license": "MIT" + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -1353,10 +6178,26 @@ "node": ">=8.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/retry-axios": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/retry-axios/-/retry-axios-2.6.0.tgz", + "integrity": "sha512-pOLi+Gdll3JekwuFjXO3fTq+L9lzMQGcSq7M5gIjExcl3Gu1hd4XXuf5o3+LuSBsaULQH7DiNbsqPd1chVpQGQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.7.0" + }, + "peerDependencies": { + "axios": "*" + } + }, "node_modules/router": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -1371,8 +6212,6 @@ }, "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", @@ -1391,10 +6230,14 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==", + "license": "ISC" + }, "node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -1409,8 +6252,6 @@ }, "node_modules/send": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", "license": "MIT", "dependencies": { "debug": "^4.3.5", @@ -1431,8 +6272,6 @@ }, "node_modules/serve-static": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", "license": "MIT", "dependencies": { "encodeurl": "^2.0.0", @@ -1450,10 +6289,27 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "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, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/side-channel": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -1471,8 +6327,6 @@ }, "node_modules/side-channel-list": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -1487,8 +6341,6 @@ }, "node_modules/side-channel-map": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -1505,8 +6357,6 @@ }, "node_modules/side-channel-weakmap": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -1522,6 +6372,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -1534,27 +6397,341 @@ "node": ">=10" } }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/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, + "license": "BSD-3-Clause", + "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, + "license": "MIT", + "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, + "license": "BSD-3-Clause" + }, + "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, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/statuses": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", "engines": { "node": ">= 0.8" } }, + "node_modules/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, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ] + }, "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==", "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "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, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": ">=4" + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/tinyexec": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", + "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", + "license": "MIT" + }, + "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, + "license": "BSD-3-Clause" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -1569,8 +6746,6 @@ }, "node_modules/toidentifier": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "license": "MIT", "engines": { "node": ">=0.6" @@ -1585,10 +6760,74 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/ts-jest": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.3.tgz", + "integrity": "sha512-KTWbK2Wot8VXargsLoxhSoEQ9OyMdzQXQoUDeIulWu2Tf7gghuBHeg+agZqVLdTOHhQHVKAaeuctBDRkhWE7hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.2", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ts-node": { "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1629,10 +6868,13 @@ } } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, "node_modules/type-is": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "license": "MIT", "dependencies": { "content-type": "^1.0.5", @@ -1644,9 +6886,7 @@ } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.2", "devOptional": true, "license": "Apache-2.0", "bin": { @@ -1657,6 +6897,20 @@ "node": ">=14.17" } }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -1664,53 +6918,401 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", - "dev": true, - "license": "MIT", - "peer": true + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "license": "MIT" }, "node_modules/unpipe": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "license": "MIT", "engines": { "node": ">= 0.8" } }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "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" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "dev": true, "license": "MIT" }, + "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, + "license": "ISC", + "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/v8-to-istanbul/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, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/vary": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "license": "MIT", "engines": { "node": ">= 0.8" } }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "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, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/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, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/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, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/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, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ws": { + "version": "8.17.1", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "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, + "license": "ISC", + "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, + "license": "ISC" + }, + "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, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yn": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 47de208..9934ee5 100644 --- a/package.json +++ b/package.json @@ -1,41 +1,65 @@ { "name": "backend", "version": "1.0.0", - "main": "index.js", - "type": "module", + "description": "Task Management System", + "main": "dist/index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "dev": "nodemon index.js", - "start": "nodemon index.js" + "build": "tsc", + "start": "ts-node index.ts", + "dev": "npm run build && node dist/index.js", + "seed": "npm run build && node dist/prisma/seed.js" }, "repository": { "type": "git", "url": "git+https://github.com/CS3203-Project/backend.git" }, "prisma": { - "seed": "node prisma/seed.js" + "seed": "node --loader ts-node/esm prisma/seed.ts" }, - "author": "", + "author": "Umesha J.A.U.C.", "license": "ISC", - "bugs": { - "url": "https://github.com/CS3203-Project/backend/issues" - }, - "homepage": "https://github.com/CS3203-Project/backend#readme", - "description": "", "dependencies": { - "@prisma/client": "^6.12.0", - "@prisma/extension-accelerate": "^2.0.2", + "@aws-sdk/client-s3": "^3.896.0", + "@aws-sdk/client-ses": "^3.872.0", + "@aws-sdk/s3-request-presigner": "^3.873.0", + "@googlemaps/google-maps-services-js": "^3.4.2", + "@prisma/client": "6.15.0", + "@types/amqplib": "^0.10.7", + "@types/aws-sdk": "^2.7.4", + "@types/multer": "^2.0.0", + "@types/pg": "^8.15.5", + "@types/socket.io": "^3.0.1", + "amqplib": "^0.10.9", + "aws-sdk": "^2.1692.0", + "axios": "^1.12.2", "bcrypt": "^6.0.0", "cors": "^2.8.5", "dotenv": "^17.2.1", "express": "^5.1.0", + "express-rate-limit": "^8.1.0", "joi": "^17.13.3", "jsonwebtoken": "^9.0.2", + "multer": "^2.0.2", "nodemon": "^3.1.10", - "prisma": "^6.12.0" + "pg": "^8.16.3", + "prisma": "^6.12.0", + "socket.io": "^4.8.1", + "socket.io-client": "^4.8.1" }, "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", + "@types/jest": "^30.0.0", + "@types/node": "^24.5.2", + "@types/socket.io": "^3.0.1", + "@types/supertest": "^6.0.3", + "cors": "^2.8.5", + "jest": "^30.1.3", + "nodemon": "^3.1.10", + "rimraf": "^6.0.1", + "supertest": "^7.1.4", + "ts-jest": "^29.4.3", "ts-node": "^10.9.2", - "typescript": "^5.8.3" + "typescript": "^5.9.2" } -} +} \ No newline at end of file diff --git a/prisma/migrations/20250728103456_init/migration.sql b/prisma/migrations/20250728103456_init/migration.sql deleted file mode 100644 index 8c7c9dc..0000000 --- a/prisma/migrations/20250728103456_init/migration.sql +++ /dev/null @@ -1,18 +0,0 @@ --- CreateTable -CREATE TABLE "User" ( - "id" SERIAL NOT NULL, - "email" TEXT NOT NULL, - "firstName" TEXT NOT NULL, - "lastName" TEXT NOT NULL, - "password" TEXT NOT NULL, - "imageUrl" TEXT, - "location" TEXT, - "phoneNumber" TEXT, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "User_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); diff --git a/prisma/migrations/20250728111351_add_address/migration.sql b/prisma/migrations/20250728111351_add_address/migration.sql deleted file mode 100644 index a1115f7..0000000 --- a/prisma/migrations/20250728111351_add_address/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "User" ADD COLUMN "address" TEXT; diff --git a/prisma/migrations/20250730071048_add_banner_url/migration.sql b/prisma/migrations/20250730071048_add_banner_url/migration.sql deleted file mode 100644 index 74c3264..0000000 --- a/prisma/migrations/20250730071048_add_banner_url/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "User" ADD COLUMN "bannerUrl" TEXT; diff --git a/prisma/migrations/20250730071403_add_gig_table/migration.sql b/prisma/migrations/20250730071403_add_gig_table/migration.sql deleted file mode 100644 index 8e477e2..0000000 --- a/prisma/migrations/20250730071403_add_gig_table/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ --- CreateTable -CREATE TABLE "gig" ( - "id" SERIAL NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "gig_pkey" PRIMARY KEY ("id") -); diff --git a/prisma/migrations/20250731084030_fix/migration.sql b/prisma/migrations/20250731084030_fix/migration.sql deleted file mode 100644 index 4c6578c..0000000 --- a/prisma/migrations/20250731084030_fix/migration.sql +++ /dev/null @@ -1,201 +0,0 @@ -/* - Warnings: - - - The primary key for the `User` table will be changed. If it partially fails, the table could be left without primary key constraint. - - You are about to drop the column `bannerUrl` on the `User` table. All the data in the column will be lost. - - You are about to drop the column `phoneNumber` on the `User` table. All the data in the column will be lost. - - You are about to drop the `gig` table. If the table is not empty, all the data it contains will be lost. - -*/ --- AlterTable -ALTER TABLE "User" DROP CONSTRAINT "User_pkey", -DROP COLUMN "bannerUrl", -DROP COLUMN "phoneNumber", -ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true, -ADD COLUMN "isEmailVerified" BOOLEAN NOT NULL DEFAULT false, -ADD COLUMN "lastLoginAt" TIMESTAMP(3), -ADD COLUMN "phone" TEXT, -ADD COLUMN "role" TEXT NOT NULL DEFAULT 'USER', -ADD COLUMN "socialmedia" TEXT[], -ALTER COLUMN "id" DROP DEFAULT, -ALTER COLUMN "id" SET DATA TYPE TEXT, -ALTER COLUMN "firstName" DROP NOT NULL, -ALTER COLUMN "lastName" DROP NOT NULL, -ADD CONSTRAINT "User_pkey" PRIMARY KEY ("id"); -DROP SEQUENCE "User_id_seq"; - --- DropTable -DROP TABLE "gig"; - --- CreateTable -CREATE TABLE "ServiceProvider" ( - "id" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "bio" TEXT, - "skills" TEXT[], - "qualifications" TEXT[], - "logoUrl" TEXT, - "averageRating" DOUBLE PRECISION, - "totalReviews" INTEGER, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "ServiceProvider_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Company" ( - "id" TEXT NOT NULL, - "providerId" TEXT NOT NULL, - "name" TEXT, - "description" TEXT, - "logo" TEXT, - "address" TEXT, - "contact" TEXT, - "socialmedia" TEXT[], - - CONSTRAINT "Company_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Category" ( - "id" TEXT NOT NULL, - "name" TEXT, - "slug" TEXT NOT NULL, - "description" TEXT, - "parentId" TEXT, - - CONSTRAINT "Category_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Service" ( - "id" TEXT NOT NULL, - "providerId" TEXT NOT NULL, - "categoryId" TEXT NOT NULL, - "title" TEXT, - "description" TEXT, - "price" DECIMAL(10,2) NOT NULL, - "currency" TEXT NOT NULL DEFAULT 'USD', - "tags" TEXT[], - "images" TEXT[], - "isActive" BOOLEAN NOT NULL DEFAULT true, - "workingTime" TEXT[], - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Service_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Schedule" ( - "id" TEXT NOT NULL, - "serviceId" TEXT NOT NULL, - "providerId" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "startTime" TEXT NOT NULL, - "endTime" TEXT NOT NULL, - "confirm" BOOLEAN NOT NULL DEFAULT false, - "queueValue" INTEGER, - - CONSTRAINT "Schedule_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Payment" ( - "id" TEXT NOT NULL, - "serviceId" TEXT NOT NULL, - "providerId" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "gateway" TEXT, - "chargeId" TEXT NOT NULL, - "amount" DECIMAL(10,2) NOT NULL, - "currency" TEXT NOT NULL, - "status" TEXT, - "paidAt" TIMESTAMP(3), - "refundedAt" TIMESTAMP(3), - "failureReason" TEXT, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "Payment_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Review" ( - "id" TEXT NOT NULL, - "reviewerId" TEXT NOT NULL, - "revieweeId" TEXT NOT NULL, - "rating" INTEGER NOT NULL, - "comment" TEXT, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Review_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "ServiceReview" ( - "id" TEXT NOT NULL, - "reviewerId" TEXT NOT NULL, - "serviceId" TEXT NOT NULL, - "rating" INTEGER NOT NULL, - "comment" TEXT, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "ServiceReview_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "ServiceProvider_userId_key" ON "ServiceProvider"("userId"); - --- CreateIndex -CREATE UNIQUE INDEX "Category_slug_key" ON "Category"("slug"); - --- CreateIndex -CREATE UNIQUE INDEX "Payment_chargeId_key" ON "Payment"("chargeId"); - --- AddForeignKey -ALTER TABLE "ServiceProvider" ADD CONSTRAINT "ServiceProvider_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Company" ADD CONSTRAINT "Company_providerId_fkey" FOREIGN KEY ("providerId") REFERENCES "ServiceProvider"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Category" ADD CONSTRAINT "Category_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Category"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Service" ADD CONSTRAINT "Service_providerId_fkey" FOREIGN KEY ("providerId") REFERENCES "ServiceProvider"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Service" ADD CONSTRAINT "Service_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Schedule" ADD CONSTRAINT "Schedule_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Schedule" ADD CONSTRAINT "Schedule_providerId_fkey" FOREIGN KEY ("providerId") REFERENCES "ServiceProvider"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Schedule" ADD CONSTRAINT "Schedule_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Payment" ADD CONSTRAINT "Payment_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Payment" ADD CONSTRAINT "Payment_providerId_fkey" FOREIGN KEY ("providerId") REFERENCES "ServiceProvider"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Payment" ADD CONSTRAINT "Payment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Review" ADD CONSTRAINT "Review_reviewerId_fkey" FOREIGN KEY ("reviewerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Review" ADD CONSTRAINT "Review_revieweeId_fkey" FOREIGN KEY ("revieweeId") REFERENCES "ServiceProvider"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "ServiceReview" ADD CONSTRAINT "ServiceReview_reviewerId_fkey" FOREIGN KEY ("reviewerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "ServiceReview" ADD CONSTRAINT "ServiceReview_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20250731092201_init/migration.sql b/prisma/migrations/20250731092201_init/migration.sql deleted file mode 100644 index 17eafe0..0000000 --- a/prisma/migrations/20250731092201_init/migration.sql +++ /dev/null @@ -1,281 +0,0 @@ -/* - Warnings: - - - You are about to drop the `Category` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `Company` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `Payment` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `Review` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `Schedule` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `Service` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `ServiceProvider` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `ServiceReview` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `User` table. If the table is not empty, all the data it contains will be lost. - -*/ --- DropForeignKey -ALTER TABLE "Category" DROP CONSTRAINT "Category_parentId_fkey"; - --- DropForeignKey -ALTER TABLE "Company" DROP CONSTRAINT "Company_providerId_fkey"; - --- DropForeignKey -ALTER TABLE "Payment" DROP CONSTRAINT "Payment_providerId_fkey"; - --- DropForeignKey -ALTER TABLE "Payment" DROP CONSTRAINT "Payment_serviceId_fkey"; - --- DropForeignKey -ALTER TABLE "Payment" DROP CONSTRAINT "Payment_userId_fkey"; - --- DropForeignKey -ALTER TABLE "Review" DROP CONSTRAINT "Review_revieweeId_fkey"; - --- DropForeignKey -ALTER TABLE "Review" DROP CONSTRAINT "Review_reviewerId_fkey"; - --- DropForeignKey -ALTER TABLE "Schedule" DROP CONSTRAINT "Schedule_providerId_fkey"; - --- DropForeignKey -ALTER TABLE "Schedule" DROP CONSTRAINT "Schedule_serviceId_fkey"; - --- DropForeignKey -ALTER TABLE "Schedule" DROP CONSTRAINT "Schedule_userId_fkey"; - --- DropForeignKey -ALTER TABLE "Service" DROP CONSTRAINT "Service_categoryId_fkey"; - --- DropForeignKey -ALTER TABLE "Service" DROP CONSTRAINT "Service_providerId_fkey"; - --- DropForeignKey -ALTER TABLE "ServiceProvider" DROP CONSTRAINT "ServiceProvider_userId_fkey"; - --- DropForeignKey -ALTER TABLE "ServiceReview" DROP CONSTRAINT "ServiceReview_reviewerId_fkey"; - --- DropForeignKey -ALTER TABLE "ServiceReview" DROP CONSTRAINT "ServiceReview_serviceId_fkey"; - --- DropTable -DROP TABLE "Category"; - --- DropTable -DROP TABLE "Company"; - --- DropTable -DROP TABLE "Payment"; - --- DropTable -DROP TABLE "Review"; - --- DropTable -DROP TABLE "Schedule"; - --- DropTable -DROP TABLE "Service"; - --- DropTable -DROP TABLE "ServiceProvider"; - --- DropTable -DROP TABLE "ServiceReview"; - --- DropTable -DROP TABLE "User"; - --- CreateTable -CREATE TABLE "user" ( - "id" VARCHAR NOT NULL, - "email" VARCHAR NOT NULL, - "Password" VARCHAR NOT NULL, - "firstName" VARCHAR, - "lastName" VARCHAR, - "phone" VARCHAR, - "ImageUrl" VARCHAR, - "location" VARCHAR, - "address" VARCHAR, - "isEmailVerified" BOOLEAN NOT NULL DEFAULT false, - "createdAt" TIMESTAMP(3), - "updatedAt" TIMESTAMP(3), - "lastLoginAt" TIMESTAMP(3), - "socialmedia" VARCHAR[], - - CONSTRAINT "user_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "service_provider" ( - "id" VARCHAR NOT NULL, - "userId" VARCHAR NOT NULL, - "bio" TEXT, - "skills" VARCHAR[], - "qualifications" VARCHAR[], - "logoUrl" VARCHAR, - "averageRating" DOUBLE PRECISION, - "totalReviews" INTEGER, - "createdAt" TIMESTAMP(3), - "updatedAt" TIMESTAMP(3), - - CONSTRAINT "service_provider_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "company" ( - "id" VARCHAR NOT NULL, - "providerId" VARCHAR NOT NULL, - "Name" VARCHAR, - "description" VARCHAR, - "logo" VARCHAR, - "address" VARCHAR, - "contact" VARCHAR, - "socialmedia" VARCHAR[], - - CONSTRAINT "company_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "category" ( - "id" VARCHAR NOT NULL, - "name" VARCHAR, - "slug" VARCHAR, - "description" TEXT, - "parentId" VARCHAR, - - CONSTRAINT "category_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "service" ( - "id" VARCHAR NOT NULL, - "providerId" VARCHAR NOT NULL, - "categoryId" VARCHAR NOT NULL, - "title" VARCHAR, - "description" TEXT, - "price" DECIMAL(10,2), - "currency" VARCHAR DEFAULT 'USD', - "tags" VARCHAR[], - "images" VARCHAR[], - "isActive" BOOLEAN NOT NULL DEFAULT true, - "workingTime" VARCHAR[], - "createdAt" TIMESTAMP(3), - "updatedAt" TIMESTAMP(3), - - CONSTRAINT "service_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "schedule" ( - "id" VARCHAR NOT NULL, - "serviceId" VARCHAR NOT NULL, - "providerId" VARCHAR NOT NULL, - "userId" VARCHAR NOT NULL, - "startTime" VARCHAR, - "endTime" VARCHAR, - "confirm" BOOLEAN NOT NULL DEFAULT false, - "QueueValue" INTEGER, - - CONSTRAINT "schedule_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "payments" ( - "id" VARCHAR NOT NULL, - "serviceId" VARCHAR NOT NULL, - "providerId" VARCHAR NOT NULL, - "userId" VARCHAR NOT NULL, - "gateway" VARCHAR, - "chargeId" VARCHAR, - "amount" DECIMAL(10,2), - "currency" VARCHAR, - "status" VARCHAR, - "paidAt" TIMESTAMP(3), - "refundedAt" TIMESTAMP(3), - "failureReason" TEXT, - "createdAt" TIMESTAMP(3), - - CONSTRAINT "payments_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "reviews" ( - "id" VARCHAR NOT NULL, - "reviewerId" VARCHAR NOT NULL, - "revieweeId" VARCHAR NOT NULL, - "rating" INTEGER, - "comment" TEXT, - "createdAt" TIMESTAMP(3), - "updatedAt" TIMESTAMP(3), - - CONSTRAINT "reviews_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "service_reviews" ( - "id" VARCHAR NOT NULL, - "reviewerId" VARCHAR NOT NULL, - "revieweeId" VARCHAR NOT NULL, - "rating" INTEGER, - "comment" TEXT, - "createdAt" TIMESTAMP(3), - "updatedAt" TIMESTAMP(3), - - CONSTRAINT "service_reviews_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "user_email_key" ON "user"("email"); - --- CreateIndex -CREATE UNIQUE INDEX "service_provider_userId_key" ON "service_provider"("userId"); - --- CreateIndex -CREATE UNIQUE INDEX "category_slug_key" ON "category"("slug"); - --- CreateIndex -CREATE UNIQUE INDEX "payments_chargeId_key" ON "payments"("chargeId"); - --- AddForeignKey -ALTER TABLE "service_provider" ADD CONSTRAINT "service_provider_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "company" ADD CONSTRAINT "company_providerId_fkey" FOREIGN KEY ("providerId") REFERENCES "service_provider"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "category" ADD CONSTRAINT "category_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "category"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "service" ADD CONSTRAINT "service_providerId_fkey" FOREIGN KEY ("providerId") REFERENCES "service_provider"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "service" ADD CONSTRAINT "service_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "category"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "schedule" ADD CONSTRAINT "schedule_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "service"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "schedule" ADD CONSTRAINT "schedule_providerId_fkey" FOREIGN KEY ("providerId") REFERENCES "service_provider"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "schedule" ADD CONSTRAINT "schedule_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "payments" ADD CONSTRAINT "payments_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "service"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "payments" ADD CONSTRAINT "payments_providerId_fkey" FOREIGN KEY ("providerId") REFERENCES "service_provider"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "payments" ADD CONSTRAINT "payments_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "reviews" ADD CONSTRAINT "reviews_reviewerId_fkey" FOREIGN KEY ("reviewerId") REFERENCES "user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "reviews" ADD CONSTRAINT "reviews_revieweeId_fkey" FOREIGN KEY ("revieweeId") REFERENCES "service_provider"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "service_reviews" ADD CONSTRAINT "service_reviews_reviewerId_fkey" FOREIGN KEY ("reviewerId") REFERENCES "user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "service_reviews" ADD CONSTRAINT "service_reviews_revieweeId_fkey" FOREIGN KEY ("revieweeId") REFERENCES "service"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20250731092602_doi/migration.sql b/prisma/migrations/20250731092602_doi/migration.sql deleted file mode 100644 index 0904c8f..0000000 --- a/prisma/migrations/20250731092602_doi/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `comment` on the `service_reviews` table. All the data in the column will be lost. - -*/ --- AlterTable -ALTER TABLE "service_reviews" DROP COLUMN "comment"; diff --git a/prisma/migrations/20250731093947_ff/migration.sql b/prisma/migrations/20250731093947_ff/migration.sql deleted file mode 100644 index 53214e7..0000000 --- a/prisma/migrations/20250731093947_ff/migration.sql +++ /dev/null @@ -1,283 +0,0 @@ -/* - Warnings: - - - You are about to drop the `category` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `company` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `payments` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `reviews` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `schedule` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `service` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `service_provider` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `service_reviews` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `user` table. If the table is not empty, all the data it contains will be lost. - -*/ --- DropForeignKey -ALTER TABLE "category" DROP CONSTRAINT "category_parentId_fkey"; - --- DropForeignKey -ALTER TABLE "company" DROP CONSTRAINT "company_providerId_fkey"; - --- DropForeignKey -ALTER TABLE "payments" DROP CONSTRAINT "payments_providerId_fkey"; - --- DropForeignKey -ALTER TABLE "payments" DROP CONSTRAINT "payments_serviceId_fkey"; - --- DropForeignKey -ALTER TABLE "payments" DROP CONSTRAINT "payments_userId_fkey"; - --- DropForeignKey -ALTER TABLE "reviews" DROP CONSTRAINT "reviews_revieweeId_fkey"; - --- DropForeignKey -ALTER TABLE "reviews" DROP CONSTRAINT "reviews_reviewerId_fkey"; - --- DropForeignKey -ALTER TABLE "schedule" DROP CONSTRAINT "schedule_providerId_fkey"; - --- DropForeignKey -ALTER TABLE "schedule" DROP CONSTRAINT "schedule_serviceId_fkey"; - --- DropForeignKey -ALTER TABLE "schedule" DROP CONSTRAINT "schedule_userId_fkey"; - --- DropForeignKey -ALTER TABLE "service" DROP CONSTRAINT "service_categoryId_fkey"; - --- DropForeignKey -ALTER TABLE "service" DROP CONSTRAINT "service_providerId_fkey"; - --- DropForeignKey -ALTER TABLE "service_provider" DROP CONSTRAINT "service_provider_userId_fkey"; - --- DropForeignKey -ALTER TABLE "service_reviews" DROP CONSTRAINT "service_reviews_revieweeId_fkey"; - --- DropForeignKey -ALTER TABLE "service_reviews" DROP CONSTRAINT "service_reviews_reviewerId_fkey"; - --- DropTable -DROP TABLE "category"; - --- DropTable -DROP TABLE "company"; - --- DropTable -DROP TABLE "payments"; - --- DropTable -DROP TABLE "reviews"; - --- DropTable -DROP TABLE "schedule"; - --- DropTable -DROP TABLE "service"; - --- DropTable -DROP TABLE "service_provider"; - --- DropTable -DROP TABLE "service_reviews"; - --- DropTable -DROP TABLE "user"; - --- CreateTable -CREATE TABLE "User" ( - "id" TEXT NOT NULL, - "email" TEXT NOT NULL, - "password" TEXT NOT NULL, - "role" TEXT NOT NULL DEFAULT 'USER', - "isActive" BOOLEAN NOT NULL DEFAULT true, - "firstName" TEXT, - "lastName" TEXT, - "phone" TEXT, - "imageUrl" TEXT, - "location" TEXT, - "address" TEXT, - "isEmailVerified" BOOLEAN NOT NULL DEFAULT false, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "lastLoginAt" TIMESTAMP(3), - "socialmedia" TEXT[], - - CONSTRAINT "User_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "ServiceProvider" ( - "id" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "bio" TEXT, - "skills" TEXT[], - "qualifications" TEXT[], - "logoUrl" TEXT, - "averageRating" DOUBLE PRECISION, - "totalReviews" INTEGER, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "ServiceProvider_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Company" ( - "id" TEXT NOT NULL, - "providerId" TEXT NOT NULL, - "name" TEXT, - "description" TEXT, - "logo" TEXT, - "address" TEXT, - "contact" TEXT, - "socialmedia" TEXT[], - - CONSTRAINT "Company_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Category" ( - "id" TEXT NOT NULL, - "name" TEXT, - "slug" TEXT NOT NULL, - "description" TEXT, - "parentId" TEXT, - - CONSTRAINT "Category_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Service" ( - "id" TEXT NOT NULL, - "providerId" TEXT NOT NULL, - "categoryId" TEXT NOT NULL, - "title" TEXT, - "description" TEXT, - "price" DECIMAL(10,2) NOT NULL, - "currency" TEXT NOT NULL DEFAULT 'USD', - "tags" TEXT[], - "images" TEXT[], - "isActive" BOOLEAN NOT NULL DEFAULT true, - "workingTime" TEXT[], - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Service_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Schedule" ( - "id" TEXT NOT NULL, - "serviceId" TEXT NOT NULL, - "providerId" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "startTime" TEXT NOT NULL, - "endTime" TEXT NOT NULL, - "confirm" BOOLEAN NOT NULL DEFAULT false, - "queueValue" INTEGER, - - CONSTRAINT "Schedule_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Payment" ( - "id" TEXT NOT NULL, - "serviceId" TEXT NOT NULL, - "providerId" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "gateway" TEXT, - "chargeId" TEXT NOT NULL, - "amount" DECIMAL(10,2) NOT NULL, - "currency" TEXT NOT NULL, - "status" TEXT, - "paidAt" TIMESTAMP(3), - "refundedAt" TIMESTAMP(3), - "failureReason" TEXT, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "Payment_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Review" ( - "id" TEXT NOT NULL, - "reviewerId" TEXT NOT NULL, - "revieweeId" TEXT NOT NULL, - "rating" INTEGER NOT NULL, - "comment" TEXT, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Review_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "ServiceReview" ( - "id" TEXT NOT NULL, - "reviewerId" TEXT NOT NULL, - "serviceId" TEXT NOT NULL, - "rating" INTEGER NOT NULL, - "comment" TEXT, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "ServiceReview_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); - --- CreateIndex -CREATE UNIQUE INDEX "ServiceProvider_userId_key" ON "ServiceProvider"("userId"); - --- CreateIndex -CREATE UNIQUE INDEX "Category_slug_key" ON "Category"("slug"); - --- CreateIndex -CREATE UNIQUE INDEX "Payment_chargeId_key" ON "Payment"("chargeId"); - --- AddForeignKey -ALTER TABLE "ServiceProvider" ADD CONSTRAINT "ServiceProvider_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Company" ADD CONSTRAINT "Company_providerId_fkey" FOREIGN KEY ("providerId") REFERENCES "ServiceProvider"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Category" ADD CONSTRAINT "Category_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Category"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Service" ADD CONSTRAINT "Service_providerId_fkey" FOREIGN KEY ("providerId") REFERENCES "ServiceProvider"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Service" ADD CONSTRAINT "Service_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Schedule" ADD CONSTRAINT "Schedule_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Schedule" ADD CONSTRAINT "Schedule_providerId_fkey" FOREIGN KEY ("providerId") REFERENCES "ServiceProvider"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Schedule" ADD CONSTRAINT "Schedule_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Payment" ADD CONSTRAINT "Payment_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Payment" ADD CONSTRAINT "Payment_providerId_fkey" FOREIGN KEY ("providerId") REFERENCES "ServiceProvider"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Payment" ADD CONSTRAINT "Payment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Review" ADD CONSTRAINT "Review_reviewerId_fkey" FOREIGN KEY ("reviewerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Review" ADD CONSTRAINT "Review_revieweeId_fkey" FOREIGN KEY ("revieweeId") REFERENCES "ServiceProvider"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "ServiceReview" ADD CONSTRAINT "ServiceReview_reviewerId_fkey" FOREIGN KEY ("reviewerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "ServiceReview" ADD CONSTRAINT "ServiceReview_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20250803042313_init/migration.sql b/prisma/migrations/20250803042313_init/migration.sql deleted file mode 100644 index aa86486..0000000 --- a/prisma/migrations/20250803042313_init/migration.sql +++ /dev/null @@ -1,3 +0,0 @@ --- AlterTable -ALTER TABLE "ServiceProvider" ADD COLUMN "IDCardUrl" TEXT, -ADD COLUMN "isVerified" BOOLEAN NOT NULL DEFAULT false; diff --git a/prisma/migrations/20250803044427_make_idcard_required/migration.sql b/prisma/migrations/20250803044427_make_idcard_required/migration.sql deleted file mode 100644 index 609a0ff..0000000 --- a/prisma/migrations/20250803044427_make_idcard_required/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -/* - Warnings: - - - Made the column `IDCardUrl` on table `ServiceProvider` required. This step will fail if there are existing NULL values in that column. - -*/ --- AlterTable -ALTER TABLE "ServiceProvider" ALTER COLUMN "IDCardUrl" SET NOT NULL; diff --git a/prisma/migrations/20250803044751_make_idcard_optional/migration.sql b/prisma/migrations/20250803044751_make_idcard_optional/migration.sql deleted file mode 100644 index 3624feb..0000000 --- a/prisma/migrations/20250803044751_make_idcard_optional/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "ServiceProvider" ALTER COLUMN "IDCardUrl" DROP NOT NULL; diff --git a/prisma/migrations/20250803045109_make_idcard_required/migration.sql b/prisma/migrations/20250803045109_make_idcard_required/migration.sql deleted file mode 100644 index 609a0ff..0000000 --- a/prisma/migrations/20250803045109_make_idcard_required/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -/* - Warnings: - - - Made the column `IDCardUrl` on table `ServiceProvider` required. This step will fail if there are existing NULL values in that column. - -*/ --- AlterTable -ALTER TABLE "ServiceProvider" ALTER COLUMN "IDCardUrl" SET NOT NULL; diff --git a/prisma/migrations/20250918042532_initialize_user_table/migration.sql b/prisma/migrations/20250918042532_initialize_user_table/migration.sql new file mode 100755 index 0000000..5f77a28 --- /dev/null +++ b/prisma/migrations/20250918042532_initialize_user_table/migration.sql @@ -0,0 +1,25 @@ +-- CreateTable +CREATE TABLE "public"."User" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "password" TEXT NOT NULL, + "firstName" TEXT, + "lastName" TEXT, + "phone" TEXT, + "address" TEXT, + "bio" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "lastLoginAt" TIMESTAMP(3), + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "public"."User"("email"); + +-- CreateIndex +CREATE INDEX "User_email_idx" ON "public"."User"("email"); + +-- CreateIndex +CREATE INDEX "User_createdAt_idx" ON "public"."User"("createdAt"); diff --git a/prisma/migrations/20250918054333_add_task_team_collaboration_models/migration.sql b/prisma/migrations/20250918054333_add_task_team_collaboration_models/migration.sql new file mode 100755 index 0000000..dc18da1 --- /dev/null +++ b/prisma/migrations/20250918054333_add_task_team_collaboration_models/migration.sql @@ -0,0 +1,209 @@ +-- CreateEnum +CREATE TYPE "public"."TeamRole" AS ENUM ('OWNER', 'ADMIN', 'MEMBER'); + +-- CreateEnum +CREATE TYPE "public"."TaskStatus" AS ENUM ('TODO', 'IN_PROGRESS', 'IN_REVIEW', 'DONE', 'CANCELLED'); + +-- CreateEnum +CREATE TYPE "public"."TaskPriority" AS ENUM ('LOW', 'MEDIUM', 'HIGH', 'URGENT'); + +-- CreateEnum +CREATE TYPE "public"."ActivityType" AS ENUM ('TASK_CREATED', 'TASK_UPDATED', 'TASK_ASSIGNED', 'TASK_COMPLETED', 'TASK_COMMENTED', 'TEAM_CREATED', 'TEAM_JOINED', 'TEAM_LEFT', 'USER_REGISTERED'); + +-- CreateEnum +CREATE TYPE "public"."EntityType" AS ENUM ('USER', 'TASK', 'TEAM', 'COMMENT'); + +-- CreateTable +CREATE TABLE "public"."Team" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Team_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."TeamMember" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "teamId" TEXT NOT NULL, + "role" "public"."TeamRole" NOT NULL DEFAULT 'MEMBER', + "joinedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "TeamMember_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."Task" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "status" "public"."TaskStatus" NOT NULL DEFAULT 'TODO', + "priority" "public"."TaskPriority" NOT NULL DEFAULT 'MEDIUM', + "dueDate" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "completedAt" TIMESTAMP(3), + "createdById" TEXT NOT NULL, + "teamId" TEXT, + + CONSTRAINT "Task_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."TaskAssignment" ( + "id" TEXT NOT NULL, + "taskId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "assignedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "assignedById" TEXT, + + CONSTRAINT "TaskAssignment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."TaskDependency" ( + "id" TEXT NOT NULL, + "taskId" TEXT NOT NULL, + "dependsOnTaskId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "TaskDependency_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."Comment" ( + "id" TEXT NOT NULL, + "content" TEXT NOT NULL, + "taskId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Comment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."Activity" ( + "id" TEXT NOT NULL, + "type" "public"."ActivityType" NOT NULL, + "description" TEXT NOT NULL, + "entityType" "public"."EntityType" NOT NULL, + "entityId" TEXT NOT NULL, + "userId" TEXT, + "teamId" TEXT, + "metadata" JSONB, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Activity_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "Team_createdAt_idx" ON "public"."Team"("createdAt"); + +-- CreateIndex +CREATE INDEX "TeamMember_userId_idx" ON "public"."TeamMember"("userId"); + +-- CreateIndex +CREATE INDEX "TeamMember_teamId_idx" ON "public"."TeamMember"("teamId"); + +-- CreateIndex +CREATE UNIQUE INDEX "TeamMember_userId_teamId_key" ON "public"."TeamMember"("userId", "teamId"); + +-- CreateIndex +CREATE INDEX "Task_createdById_idx" ON "public"."Task"("createdById"); + +-- CreateIndex +CREATE INDEX "Task_teamId_idx" ON "public"."Task"("teamId"); + +-- CreateIndex +CREATE INDEX "Task_status_idx" ON "public"."Task"("status"); + +-- CreateIndex +CREATE INDEX "Task_priority_idx" ON "public"."Task"("priority"); + +-- CreateIndex +CREATE INDEX "Task_dueDate_idx" ON "public"."Task"("dueDate"); + +-- CreateIndex +CREATE INDEX "Task_createdAt_idx" ON "public"."Task"("createdAt"); + +-- CreateIndex +CREATE INDEX "TaskAssignment_taskId_idx" ON "public"."TaskAssignment"("taskId"); + +-- CreateIndex +CREATE INDEX "TaskAssignment_userId_idx" ON "public"."TaskAssignment"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "TaskAssignment_taskId_userId_key" ON "public"."TaskAssignment"("taskId", "userId"); + +-- CreateIndex +CREATE INDEX "TaskDependency_taskId_idx" ON "public"."TaskDependency"("taskId"); + +-- CreateIndex +CREATE INDEX "TaskDependency_dependsOnTaskId_idx" ON "public"."TaskDependency"("dependsOnTaskId"); + +-- CreateIndex +CREATE UNIQUE INDEX "TaskDependency_taskId_dependsOnTaskId_key" ON "public"."TaskDependency"("taskId", "dependsOnTaskId"); + +-- CreateIndex +CREATE INDEX "Comment_taskId_idx" ON "public"."Comment"("taskId"); + +-- CreateIndex +CREATE INDEX "Comment_userId_idx" ON "public"."Comment"("userId"); + +-- CreateIndex +CREATE INDEX "Comment_createdAt_idx" ON "public"."Comment"("createdAt"); + +-- CreateIndex +CREATE INDEX "Activity_entityType_entityId_idx" ON "public"."Activity"("entityType", "entityId"); + +-- CreateIndex +CREATE INDEX "Activity_userId_idx" ON "public"."Activity"("userId"); + +-- CreateIndex +CREATE INDEX "Activity_teamId_idx" ON "public"."Activity"("teamId"); + +-- CreateIndex +CREATE INDEX "Activity_createdAt_idx" ON "public"."Activity"("createdAt"); + +-- AddForeignKey +ALTER TABLE "public"."TeamMember" ADD CONSTRAINT "TeamMember_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."TeamMember" ADD CONSTRAINT "TeamMember_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "public"."Team"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Task" ADD CONSTRAINT "Task_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Task" ADD CONSTRAINT "Task_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "public"."Team"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."TaskAssignment" ADD CONSTRAINT "TaskAssignment_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "public"."Task"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."TaskAssignment" ADD CONSTRAINT "TaskAssignment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."TaskDependency" ADD CONSTRAINT "TaskDependency_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "public"."Task"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."TaskDependency" ADD CONSTRAINT "TaskDependency_dependsOnTaskId_fkey" FOREIGN KEY ("dependsOnTaskId") REFERENCES "public"."Task"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Comment" ADD CONSTRAINT "Comment_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "public"."Task"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Comment" ADD CONSTRAINT "Comment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Activity" ADD CONSTRAINT "Activity_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Activity" ADD CONSTRAINT "Activity_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "public"."Team"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Activity" ADD CONSTRAINT "Activity_entityId_fkey" FOREIGN KEY ("entityId") REFERENCES "public"."Task"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20250918105039_fix_activity_relations/migration.sql b/prisma/migrations/20250918105039_fix_activity_relations/migration.sql new file mode 100755 index 0000000..1a44659 --- /dev/null +++ b/prisma/migrations/20250918105039_fix_activity_relations/migration.sql @@ -0,0 +1,2 @@ +-- DropForeignKey +ALTER TABLE "public"."Activity" DROP CONSTRAINT "Activity_entityId_fkey"; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml old mode 100644 new mode 100755 diff --git a/prisma/schema.prisma b/prisma/schema.prisma old mode 100644 new mode 100755 index 988be33..34033eb --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1,68 +1,74 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - -// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? -// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init - generator client { provider = "prisma-client-js" + previewFeatures = ["postgresqlExtensions"] } datasource db { provider = "postgresql" url = env("DATABASE_URL") + extensions = [pgvector(map: "vector"), postgis(map: "postgis")] } model User { - id String @id @default(cuid()) - email String @unique - password String - role String @default("USER") // USER, PROVIDER, ADMIN - isActive Boolean @default(true) - firstName String? - lastName String? - phone String? - imageUrl String? - location String? - address String? - isEmailVerified Boolean @default(false) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - lastLoginAt DateTime? - socialmedia String[] - - serviceProvider ServiceProvider? - schedules Schedule[] - payments Payment[] - reviewsWritten Review[] @relation("writtenReviews") - serviceReviewsWritten ServiceReview[] @relation("writtenServiceReviews") + id String @id @default(cuid()) + email String @unique + password String + role String @default("USER") + isActive Boolean @default(true) + firstName String? + lastName String? + phone String? + imageUrl String? + location String? + address String? + isEmailVerified Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + lastLoginAt DateTime? + socialmedia String[] + messagesSent Message[] @relation("MessagesSent") + messagesReceived Message[] @relation("MessagesReceived") + payments Payment[] + schedules Schedule[] + serviceProvider ServiceProvider? + notification Notification[] + customerReviewsWritten CustomerReview[] @relation("CustomerReviewReviewer") + customerReviewsReceived CustomerReview[] @relation("CustomerReviewReviewee") + writtenServiceReviews ServiceReview[] @relation("writtenServiceReviews") + + @@index([email]) + @@index([role]) + @@index([isActive]) + @@index([createdAt]) } model ServiceProvider { - id String @id @default(cuid()) - userId String @unique - user User @relation(fields: [userId], references: [id]) - + id String @id @default(cuid()) + userId String @unique bio String? skills String[] qualifications String[] logoUrl String? averageRating Float? totalReviews Int? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - isVerified Boolean @default(false) - IDCardUrl String // Required ID card image URL - + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + IDCardUrl String + isVerified Boolean @default(true) companies Company[] - services Service[] - schedules Schedule[] payments Payment[] - reviews Review[] @relation("receivedReviews") + schedules Schedule[] + services Service[] + user User @relation(fields: [userId], references: [id]) + + @@index([userId]) + @@index([isVerified]) + @@index([averageRating]) + @@index([createdAt]) } model Company { - id String @id @default(cuid()) + id String @id @default(cuid()) providerId String name String? description String? @@ -70,7 +76,6 @@ model Company { address String? contact String? socialmedia String[] - provider ServiceProvider @relation(fields: [providerId], references: [id]) } @@ -80,91 +85,175 @@ model Category { slug String @unique description String? parentId String? - parent Category? @relation("CategoryHierarchy", fields: [parentId], references: [id]) children Category[] @relation("CategoryHierarchy") services Service[] } model Service { - id String @id @default(cuid()) + id String @id @default(cuid()) providerId String categoryId String title String? description String? - price Decimal @db.Decimal(10, 2) - currency String @default("USD") + price Decimal @db.Decimal(10, 2) + currency String @default("RS") tags String[] images String[] - isActive Boolean @default(true) + videoUrl String? + isActive Boolean @default(true) workingTime String[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - provider ServiceProvider @relation(fields: [providerId], references: [id]) - category Category @relation(fields: [categoryId], references: [id]) - schedules Schedule[] + // Location fields + latitude Float? + longitude Float? + address String? + city String? + state String? + country String? + postalCode String? + serviceRadiusKm Float? @default(10) + locationLastUpdated DateTime? + + // Semantic search fields + titleEmbedding Unsupported("vector(768)")? + descriptionEmbedding Unsupported("vector(768)")? + tagsEmbedding Unsupported("vector(768)")? + combinedEmbedding Unsupported("vector(768)")? + embeddingUpdatedAt DateTime? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt payments Payment[] - reviews ServiceReview[] + schedules Schedule[] + conversations Conversation[] + category Category @relation(fields: [categoryId], references: [id]) + provider ServiceProvider @relation(fields: [providerId], references: [id]) + serviceReviews ServiceReview[] + + @@index([isActive]) + @@index([categoryId]) + @@index([providerId]) + @@index([createdAt]) + @@index([latitude, longitude]) + @@index([city]) + @@index([state]) } model Schedule { - id String @id @default(cuid()) - serviceId String - providerId String - userId String - startTime String - endTime String - confirm Boolean @default(false) - queueValue Int? - - service Service @relation(fields: [serviceId], references: [id]) - provider ServiceProvider @relation(fields: [providerId], references: [id]) - user User @relation(fields: [userId], references: [id]) + id String @id @default(cuid()) + serviceId String + providerId String + userId String + startTime String + endTime String + customerConfirmation Boolean @default(false) + providerConfirmation Boolean @default(false) + serviceFee Decimal? @db.Decimal(10, 2) + currency String @default("RS") + queueValue Int? + provider ServiceProvider @relation(fields: [providerId], references: [id]) + service Service @relation(fields: [serviceId], references: [id]) + user User @relation(fields: [userId], references: [id]) } model Payment { - id String @id @default(cuid()) + id String @id @default(cuid()) serviceId String providerId String userId String gateway String? - chargeId String @unique - amount Decimal @db.Decimal(10, 2) + chargeId String @unique + amount Decimal @db.Decimal(10, 2) currency String status String? paidAt DateTime? refundedAt DateTime? failureReason String? - createdAt DateTime @default(now()) + createdAt DateTime @default(now()) + provider ServiceProvider @relation(fields: [providerId], references: [id]) + service Service @relation(fields: [serviceId], references: [id]) + user User @relation(fields: [userId], references: [id]) +} + - service Service @relation(fields: [serviceId], references: [id]) - provider ServiceProvider @relation(fields: [providerId], references: [id]) - user User @relation(fields: [userId], references: [id]) +model ServiceReview { + id String @id @default(cuid()) + reviewerId String + serviceId String + rating Int + comment String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + reviewer User @relation("writtenServiceReviews", fields: [reviewerId], references: [id]) + service Service @relation(fields: [serviceId], references: [id]) } -model Review { - id String @id @default(cuid()) - reviewerId String - revieweeId String - rating Int - comment String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - reviewer User @relation("writtenReviews", fields: [reviewerId], references: [id]) - reviewee ServiceProvider @relation("receivedReviews", fields: [revieweeId], references: [id]) +model Notification { + id String @id @default(cuid()) + userId String? + to String + subject String + html String + emailType EmailType + sentAt DateTime? + createdAt DateTime @default(now()) + isRead Boolean @default(false) + user User? @relation(fields: [userId], references: [id]) + + @@index([userId]) + @@index([emailType]) + @@index([sentAt]) + @@map("notification") } + model Admin { + id Int @id @default(autoincrement()) + username String @unique + password String + firstName String + lastName String + } -model ServiceReview { - id String @id @default(cuid()) - reviewerId String - serviceId String - rating Int - comment String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - reviewer User @relation("writtenServiceReviews", fields: [reviewerId], references: [id]) - service Service @relation(fields: [serviceId], references: [id]) -} \ No newline at end of file +model Conversation { + id String @id @default(uuid()) + userIds String[] + title String? + serviceId String? + service Service? @relation(fields: [serviceId], references: [id]) + messages Message[] +} + +model Message { + id String @id @default(uuid()) + content String + fromId String + toId String + conversationId String + createdAt DateTime @default(now()) + receivedAt DateTime? + conversation Conversation @relation(fields: [conversationId], references: [id]) + from User @relation("MessagesSent", fields: [fromId], references: [id]) + to User @relation("MessagesReceived", fields: [toId], references: [id]) +} + +model CustomerReview { + id String @id @default(cuid()) + reviewerId String + revieweeId String + rating Int + comment String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + reviewer User @relation("CustomerReviewReviewer", fields: [reviewerId], references: [id]) + reviewee User @relation("CustomerReviewReviewee", fields: [revieweeId], references: [id]) +} + +enum EmailType { + BOOKING_CONFIRMATION @map("BOOKING_CONFIRMATION") + BOOKING_REMINDER @map("BOOKING_REMINDER") + BOOKING_CANCELLATION_MODIFICATION @map("BOOKING_CANCELLATION_MODIFICATION") + NEW_MESSAGE_OR_REVIEW @map("NEW_MESSAGE_OR_REVIEW") + OTHER @map("OTHER") + + @@map("EmailType") +} diff --git a/prisma/seed.js b/prisma/seed.js deleted file mode 100644 index 22946e7..0000000 --- a/prisma/seed.js +++ /dev/null @@ -1,24 +0,0 @@ - -import { PrismaClient } from "@prisma/client"; -import { hashPassword } from "../src/utils/hash.js"; - -const prisma = new PrismaClient(); - -async function seed() { - const adminPassword = await hashPassword("admin123"); - const userPassword = await hashPassword("user123"); - await prisma.user.createMany({ - data: [] - }); -} - -seed() - .then(() => { - console.log("Seeding completed successfully."); - }) - .catch((error) => { - console.error("Error during seeding:", error); - }) - .finally(async () => { - await prisma.$disconnect(); - }); \ No newline at end of file diff --git a/rename_email_queue_to_notification_add_isread.sql b/rename_email_queue_to_notification_add_isread.sql new file mode 100755 index 0000000..71aed2e --- /dev/null +++ b/rename_email_queue_to_notification_add_isread.sql @@ -0,0 +1,28 @@ +-- Rename email_queue table to notification and add isRead column +-- Migration: Rename EmailQueue table and add read status tracking +-- Generated on: 2025-10-06 +-- Description: Database migration to rename email_queue to notification table and add isRead boolean field + +-- Step 1: Rename the table from email_queue to notification +ALTER TABLE "email_queue" RENAME TO "notification"; + +-- Step 2: Add the isRead column with a default value of false +-- This column tracks whether the notification has been read by the recipient +ALTER TABLE "notification" ADD COLUMN "isRead" BOOLEAN NOT NULL DEFAULT false; + +-- Step 3: Verify the migration (optional verification queries) +-- You can uncomment and run these queries to verify the migration: + +-- Check table structure: +-- SELECT column_name, data_type, is_nullable, column_default +-- FROM information_schema.columns +-- WHERE table_name = 'notification' +-- ORDER BY ordinal_position; + +-- Check if indexes were renamed properly (PostgreSQL should handle this automatically): +-- SELECT indexname, indexdef +-- FROM pg_indexes +-- WHERE tablename = 'notification'; + +-- Check existing data count to ensure no data loss: +-- SELECT COUNT(*) as notification_count FROM notification; diff --git a/scripts/generate-missing-embeddings.js b/scripts/generate-missing-embeddings.js new file mode 100755 index 0000000..6754d1a --- /dev/null +++ b/scripts/generate-missing-embeddings.js @@ -0,0 +1,228 @@ +/** + * Migration script to generate embeddings for existing services + * Run this script to populate embeddings for services that don't have them + */ + +import { PrismaClient } from '@prisma/client'; +import { embeddingService } from '../dist/src/services/embedding.service.js'; + +const prisma = new PrismaClient(); + +async function generateMissingEmbeddings() { + console.log('🚀 Starting migration: Generate missing embeddings for services'); + console.log('================================================'); + + try { + // Get count of services without embeddings + const totalServicesWithoutEmbeddings = await prisma.$queryRaw` + SELECT COUNT(*) as count + FROM "Service" + WHERE "combinedEmbedding" IS NULL + AND "isActive" = true + `; + + const totalCount = parseInt(totalServicesWithoutEmbeddings[0].count); + console.log(`📊 Found ${totalCount} services without embeddings`); + + if (totalCount === 0) { + console.log('✅ All active services already have embeddings!'); + return; + } + + console.log('âš ī¸ This process will take time due to API rate limits (15 requests/minute)'); + console.log(`âąī¸ Estimated time: ${Math.ceil((totalCount * 5) / 60)} minutes\n`); + + const batchSize = 10; // Process in small batches + let processedCount = 0; + let successCount = 0; + let errorCount = 0; + + while (processedCount < totalCount) { + console.log(`\nđŸ“Ļ Processing batch ${Math.floor(processedCount / batchSize) + 1}...`); + + // Get next batch of services without embeddings + const services = await prisma.$queryRaw` + SELECT id, title, description, tags + FROM "Service" + WHERE "combinedEmbedding" IS NULL + AND "isActive" = true + ORDER BY "createdAt" DESC + LIMIT ${batchSize} + `; + + if (services.length === 0) { + console.log('🎉 No more services to process!'); + break; + } + + // Process each service in the batch + for (let i = 0; i < services.length; i++) { + const service = services[i]; + processedCount++; + + try { + console.log(`\nâŗ [${processedCount}/${totalCount}] Processing: "${service.title || 'Untitled Service'}"`); + console.log(` Service ID: ${service.id}`); + + // Generate embeddings + const embeddings = await embeddingService.generateServiceEmbeddings({ + title: service.title || '', + description: service.description || '', + tags: service.tags || [] + }); + + // Convert embeddings to vector format strings + const titleVector = `[${embeddings.titleEmbedding.join(',')}]`; + const descriptionVector = `[${embeddings.descriptionEmbedding.join(',')}]`; + const tagsVector = `[${embeddings.tagsEmbedding.join(',')}]`; + const combinedVector = `[${embeddings.combinedEmbedding.join(',')}]`; + + console.log(` 📐 Vector dimensions: ${embeddings.combinedEmbedding.length}`); + + // Update service with embeddings + await prisma.$executeRaw` + UPDATE "Service" + SET + "titleEmbedding" = ${titleVector}::vector, + "descriptionEmbedding" = ${descriptionVector}::vector, + "tagsEmbedding" = ${tagsVector}::vector, + "combinedEmbedding" = ${combinedVector}::vector, + "embeddingUpdatedAt" = NOW() + WHERE id = ${service.id} + `; + + successCount++; + console.log(` ✅ Success! Embeddings generated and saved.`); + + // Rate limiting: Wait 5 seconds between requests for free tier + if (processedCount < totalCount) { + console.log(` âąī¸ Waiting 5 seconds (rate limit)...`); + await new Promise(resolve => setTimeout(resolve, 5000)); + } + + } catch (error) { + errorCount++; + console.error(` ❌ Failed to process service ${service.id}:`); + console.error(` Error: ${error.message}`); + + // If it's a rate limit error, wait longer + if (error.message.includes('Daily API limit reached')) { + console.log('đŸšĢ Daily API limit reached. Stopping migration.'); + console.log('💡 Please run this script again tomorrow to continue.'); + break; + } + + // Continue with next service for other errors + continue; + } + } + + // Progress update + const remainingCount = totalCount - processedCount; + const estimatedTimeRemaining = Math.ceil((remainingCount * 5) / 60); + + console.log(`\n📈 Progress: ${processedCount}/${totalCount} services processed`); + console.log(`✅ Successful: ${successCount} | ❌ Failed: ${errorCount}`); + + if (remainingCount > 0) { + console.log(`âąī¸ Estimated time remaining: ${estimatedTimeRemaining} minutes`); + } + } + + console.log('\n================================================'); + console.log('🎉 Migration completed!'); + console.log(`📊 Total services processed: ${processedCount}`); + console.log(`✅ Successfully updated: ${successCount}`); + console.log(`❌ Failed: ${errorCount}`); + console.log(`📈 Success rate: ${((successCount / processedCount) * 100).toFixed(1)}%`); + + if (errorCount > 0) { + console.log('\n💡 Tip: You can run this script again to retry failed services.'); + } + + } catch (error) { + console.error('đŸ’Ĩ Migration failed:', error); + console.error('Error details:', error.message); + process.exit(1); + } finally { + await prisma.$disconnect(); + } +} + +// Check if service is ready +async function checkPrerequisites() { + try { + // Test database connection + await prisma.$queryRaw`SELECT 1`; + console.log('✅ Database connection successful'); + + // Check if pgvector extension is available + const pgvectorCheck = await prisma.$queryRaw` + SELECT * FROM pg_extension WHERE extname = 'vector' + `; + + if (pgvectorCheck.length === 0) { + throw new Error('pgvector extension is not installed in the database'); + } + console.log('✅ pgvector extension is available'); + + // Test embedding service + const testEmbedding = await embeddingService.generateEmbedding('test'); + if (!testEmbedding || testEmbedding.length === 0) { + throw new Error('Embedding service is not working properly'); + } + console.log('✅ Embedding service is working'); + console.log(`📏 Embedding dimension: ${testEmbedding.length}`); + + return true; + } catch (error) { + console.error('❌ Prerequisites check failed:', error.message); + return false; + } +} + +// Main execution +async function main() { + console.log('🔍 Checking prerequisites...'); + + const prerequisitesOk = await checkPrerequisites(); + if (!prerequisitesOk) { + console.log('\n💡 Please fix the issues above before running the migration.'); + process.exit(1); + } + + console.log('\n✅ All prerequisites met. Starting migration...\n'); + + // Ask for confirmation in production + if (process.env.NODE_ENV === 'production') { + console.log('âš ī¸ You are running this in PRODUCTION mode.'); + console.log('🚨 This will modify the database and use API quota.'); + console.log('💰 Make sure you have sufficient API quota for the Gemini embedding service.'); + console.log('\nPress Ctrl+C to cancel, or wait 10 seconds to continue...'); + + await new Promise(resolve => setTimeout(resolve, 10000)); + } + + await generateMissingEmbeddings(); +} + +// Handle graceful shutdown +process.on('SIGINT', async () => { + console.log('\n\n🛑 Migration interrupted by user'); + console.log('💾 Any completed embeddings have been saved to the database'); + await prisma.$disconnect(); + process.exit(0); +}); + +process.on('SIGTERM', async () => { + console.log('\n\n🛑 Migration terminated'); + await prisma.$disconnect(); + process.exit(0); +}); + +// Run the migration +main().catch(async (error) => { + console.error('đŸ’Ĩ Unexpected error:', error); + await prisma.$disconnect(); + process.exit(1); +}); \ No newline at end of file diff --git a/scripts/generate-missing-embeddings.ts b/scripts/generate-missing-embeddings.ts new file mode 100755 index 0000000..d1e5148 --- /dev/null +++ b/scripts/generate-missing-embeddings.ts @@ -0,0 +1,237 @@ +#!/usr/bin/env ts-node +/** + * TypeScript Migration script to generate embeddings for existing services + * Usage: npx ts-node scripts/generate-missing-embeddings.ts + */ + +import { PrismaClient } from '@prisma/client'; +import { embeddingService } from '../src/services/embedding.service.js'; + +const prisma = new PrismaClient(); + +async function generateMissingEmbeddings() { + console.log('🚀 Starting migration: Generate missing embeddings for services'); + console.log('================================================'); + + try { + // Get count of services without embeddings + const totalServicesWithoutEmbeddings = await prisma.$queryRaw<[{count: bigint}]>` + SELECT COUNT(*) as count + FROM "Service" + WHERE "combinedEmbedding" IS NULL + AND "isActive" = true + `; + + const totalCount = Number(totalServicesWithoutEmbeddings[0].count); + console.log(`📊 Found ${totalCount} services without embeddings`); + + if (totalCount === 0) { + console.log('✅ All active services already have embeddings!'); + return; + } + + console.log('âš ī¸ This process will take time due to API rate limits (15 requests/minute)'); + console.log(`âąī¸ Estimated time: ${Math.ceil((totalCount * 5) / 60)} minutes\n`); + + const batchSize = 5; // Smaller batch for free tier + let processedCount = 0; + let successCount = 0; + let errorCount = 0; + + while (processedCount < totalCount) { + console.log(`\nđŸ“Ļ Processing batch ${Math.floor(processedCount / batchSize) + 1}...`); + + // Get next batch of services without embeddings + const services = await prisma.$queryRaw>` + SELECT id, title, description, tags + FROM "Service" + WHERE "combinedEmbedding" IS NULL + AND "isActive" = true + ORDER BY "createdAt" DESC + LIMIT ${batchSize} + `; + + if (services.length === 0) { + console.log('🎉 No more services to process!'); + break; + } + + // Process each service in the batch + for (let i = 0; i < services.length; i++) { + const service = services[i]; + processedCount++; + + try { + console.log(`\nâŗ [${processedCount}/${totalCount}] Processing: "${service.title || 'Untitled Service'}"`); + console.log(` Service ID: ${service.id}`); + + // Generate embeddings + const embeddings = await embeddingService.generateServiceEmbeddings({ + title: service.title || '', + description: service.description || '', + tags: service.tags || [] + }); + + // Update service with embeddings + await prisma.$executeRaw` + UPDATE "Service" + SET + "titleEmbedding" = ${embeddings.titleEmbedding}::vector, + "descriptionEmbedding" = ${embeddings.descriptionEmbedding}::vector, + "tagsEmbedding" = ${embeddings.tagsEmbedding}::vector, + "combinedEmbedding" = ${embeddings.combinedEmbedding}::vector, + "embeddingUpdatedAt" = NOW() + WHERE id = ${service.id} + `; + + successCount++; + console.log(` ✅ Success! Embeddings generated and saved.`); + + // Rate limiting: Wait 5 seconds between requests for free tier + if (processedCount < totalCount) { + console.log(` âąī¸ Waiting 5 seconds (rate limit)...`); + await new Promise(resolve => setTimeout(resolve, 5000)); + } + + } catch (error: any) { + errorCount++; + console.error(` ❌ Failed to process service ${service.id}:`); + console.error(` Error: ${error.message}`); + + // If it's a rate limit error, wait longer + if (error.message?.includes('Daily API limit reached')) { + console.log('đŸšĢ Daily API limit reached. Stopping migration.'); + console.log('💡 Please run this script again tomorrow to continue.'); + break; + } + + // If it's a quota error, wait 1 minute and continue + if (error.message?.includes('quota') || error.message?.includes('rate limit')) { + console.log('â¸ī¸ Rate limit hit. Waiting 60 seconds before continuing...'); + await new Promise(resolve => setTimeout(resolve, 60000)); + } + + // Continue with next service for other errors + continue; + } + } + + // Progress update + const remainingCount = totalCount - processedCount; + const estimatedTimeRemaining = Math.ceil((remainingCount * 5) / 60); + + console.log(`\n📈 Progress: ${processedCount}/${totalCount} services processed`); + console.log(`✅ Successful: ${successCount} | ❌ Failed: ${errorCount}`); + + if (remainingCount > 0) { + console.log(`âąī¸ Estimated time remaining: ${estimatedTimeRemaining} minutes`); + } + } + + console.log('\n================================================'); + console.log('🎉 Migration completed!'); + console.log(`📊 Total services processed: ${processedCount}`); + console.log(`✅ Successfully updated: ${successCount}`); + console.log(`❌ Failed: ${errorCount}`); + + if (processedCount > 0) { + console.log(`📈 Success rate: ${((successCount / processedCount) * 100).toFixed(1)}%`); + } + + if (errorCount > 0) { + console.log('\n💡 Tip: You can run this script again to retry failed services.'); + } + + } catch (error: any) { + console.error('đŸ’Ĩ Migration failed:', error); + console.error('Error details:', error.message); + process.exit(1); + } finally { + await prisma.$disconnect(); + } +} + +// Check if service is ready +async function checkPrerequisites() { + try { + // Test database connection + await prisma.$queryRaw`SELECT 1`; + console.log('✅ Database connection successful'); + + // Check if pgvector extension is available + const pgvectorCheck = await prisma.$queryRaw>` + SELECT * FROM pg_extension WHERE extname = 'vector' + `; + + if (pgvectorCheck.length === 0) { + throw new Error('pgvector extension is not installed in the database'); + } + console.log('✅ pgvector extension is available'); + + // Test embedding service + const testEmbedding = await embeddingService.generateEmbedding('test'); + if (!testEmbedding || testEmbedding.length === 0) { + throw new Error('Embedding service is not working properly'); + } + console.log('✅ Embedding service is working'); + console.log(`📏 Embedding dimension: ${testEmbedding.length}`); + + return true; + } catch (error: any) { + console.error('❌ Prerequisites check failed:', error.message); + return false; + } +} + +// Main execution +async function main() { + console.log('🔍 Checking prerequisites...'); + + const prerequisitesOk = await checkPrerequisites(); + if (!prerequisitesOk) { + console.log('\n💡 Please fix the issues above before running the migration.'); + process.exit(1); + } + + console.log('\n✅ All prerequisites met. Starting migration...\n'); + + // Ask for confirmation in production + if (process.env.NODE_ENV === 'production') { + console.log('âš ī¸ You are running this in PRODUCTION mode.'); + console.log('🚨 This will modify the database and use API quota.'); + console.log('💰 Make sure you have sufficient API quota for the Gemini embedding service.'); + console.log('\nPress Ctrl+C to cancel, or wait 10 seconds to continue...'); + + await new Promise(resolve => setTimeout(resolve, 10000)); + } + + await generateMissingEmbeddings(); +} + +// Handle graceful shutdown +process.on('SIGINT', async () => { + console.log('\n\n🛑 Migration interrupted by user'); + console.log('💾 Any completed embeddings have been saved to the database'); + await prisma.$disconnect(); + process.exit(0); +}); + +process.on('SIGTERM', async () => { + console.log('\n\n🛑 Migration terminated'); + await prisma.$disconnect(); + process.exit(0); +}); + +// Run the migration if this script is executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + main().catch(async (error) => { + console.error('đŸ’Ĩ Unexpected error:', error); + await prisma.$disconnect(); + process.exit(1); + }); +} \ No newline at end of file diff --git a/src/Admin/controllers/admin.controller.ts b/src/Admin/controllers/admin.controller.ts new file mode 100644 index 0000000..6678344 --- /dev/null +++ b/src/Admin/controllers/admin.controller.ts @@ -0,0 +1,316 @@ +import type { Request, Response } from 'express'; +import jwt from 'jsonwebtoken'; +import { adminService } from '../services/admin.service.js'; + +const JWT_SECRET = process.env.JWT_SECRET || 'fallback-secret-key'; +const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '24h'; + +export class AdminController { + async register(req: Request, res: Response): Promise { + try { + const { username, password, firstName, lastName } = req.body; + + // Check if admin already exists + const existingAdmin = await adminService.getAdminByUsername(username); + if (existingAdmin) { + res.status(400).json({ + success: false, + message: 'Admin with this username already exists', + }); + return; + } + + const admin = await adminService.createAdmin({ + username, + password, + firstName, + lastName, + }); + + res.status(201).json({ + success: true, + message: 'Admin created successfully', + data: admin, + }); + } catch (error: any) { + console.error('Admin registration error:', error); + res.status(500).json({ + success: false, + message: 'Internal server error', + error: error.message, + }); + } + } + + async login(req: Request, res: Response): Promise { + try { + const { username, password } = req.body; + + const admin = await adminService.loginAdmin({ username, password }); + if (!admin) { + res.status(401).json({ + success: false, + message: 'Invalid username or password', + }); + return; + } + + // Generate JWT token + const token = jwt.sign( + { + adminId: admin.id, + username: admin.username, + role: 'ADMIN', + }, + JWT_SECRET, + { expiresIn: '1h' } + ); + + res.status(200).json({ + success: true, + message: 'Login successful', + data: { + admin, + token, + }, + }); + } catch (error: any) { + console.error('Admin login error:', error); + res.status(500).json({ + success: false, + message: 'Internal server error', + error: error.message, + }); + } + } + + async getProfile(req: Request, res: Response): Promise { + try { + const adminId = (req as any).admin?.id; + + const admin = await adminService.getAdminById(adminId); + if (!admin) { + res.status(404).json({ + success: false, + message: 'Admin not found', + }); + return; + } + + res.status(200).json({ + success: true, + data: admin, + }); + } catch (error: any) { + console.error('Get admin profile error:', error); + res.status(500).json({ + success: false, + message: 'Internal server error', + error: error.message, + }); + } + } + + async getAllAdmins(req: Request, res: Response): Promise { + try { + const admins = await adminService.getAllAdmins(); + + res.status(200).json({ + success: true, + data: admins, + }); + } catch (error: any) { + console.error('Get all admins error:', error); + res.status(500).json({ + success: false, + message: 'Internal server error', + error: error.message, + }); + } + } + + async updateProfile(req: Request, res: Response): Promise { + try { + const adminId = (req as any).admin?.id; + const { username, password, firstName, lastName } = req.body; + + // Check if at least one field is provided + if (!username && !password && !firstName && !lastName) { + res.status(400).json({ + success: false, + message: 'At least one field must be provided for update', + }); + return; + } + + const updateData: any = {}; + if (username) updateData.username = username; + if (password) updateData.password = password; + if (firstName) updateData.firstName = firstName; + if (lastName) updateData.lastName = lastName; + + const updatedAdmin = await adminService.updateAdmin(adminId, updateData); + + if (!updatedAdmin) { + res.status(404).json({ + success: false, + message: 'Admin not found', + }); + return; + } + + res.status(200).json({ + success: true, + message: 'Profile updated successfully', + data: updatedAdmin, + }); + } catch (error: any) { + console.error('Update admin profile error:', error); + + if (error.message === 'Username already exists') { + res.status(400).json({ + success: false, + message: 'Username already exists', + }); + return; + } + + res.status(500).json({ + success: false, + message: 'Internal server error', + error: error.message, + }); + } + } + + async getAllServiceProviders(req: Request, res: Response): Promise { + try { + const serviceProviders = await adminService.getAllServiceProvidersWithDetails(); + + res.status(200).json({ + success: true, + message: 'Service providers fetched successfully', + data: serviceProviders, + count: serviceProviders.length, + }); + } catch (error: any) { + console.error('Get all service providers error:', error); + res.status(500).json({ + success: false, + message: 'Internal server error', + error: error.message, + }); + } + } + + async updateServiceProviderVerification(req: Request, res: Response): Promise { + try { + const { providerId } = req.params; + const { isVerified } = req.body; + + // Validate the input + if (typeof isVerified !== 'boolean') { + res.status(400).json({ + success: false, + message: 'isVerified must be a boolean value (true for approve, false for reject)', + }); + return; + } + + if (!providerId) { + res.status(400).json({ + success: false, + message: 'Provider ID is required', + }); + return; + } + + const updatedProvider = await adminService.updateServiceProviderVerification(providerId, isVerified); + + res.status(200).json({ + success: true, + message: `Service provider ${isVerified ? 'approved' : 'rejected'} successfully`, + data: updatedProvider, + }); + } catch (error: any) { + console.error('Update service provider verification error:', error); + + if (error.message === 'Service provider not found') { + res.status(404).json({ + success: false, + message: 'Service provider not found', + }); + return; + } + + res.status(500).json({ + success: false, + message: 'Internal server error', + error: error.message, + }); + } + } + + async getCustomerCount(req: Request, res: Response): Promise { + try { + const customerCount = await adminService.getCustomerCount(); + + res.status(200).json({ + success: true, + message: 'Customer count fetched successfully', + data: { + count: customerCount, + }, + }); + } catch (error: any) { + console.error('Get customer count error:', error); + res.status(500).json({ + success: false, + message: 'Internal server error', + error: error.message, + }); + } + } + + async getAllCustomers(req: Request, res: Response): Promise { + try { + const customers = await adminService.getAllCustomers(); + + res.status(200).json({ + success: true, + message: 'All customers fetched successfully', + data: customers, + count: customers.length, + }); + } catch (error: any) { + console.error('Get all customers error:', error); + res.status(500).json({ + success: false, + message: 'Internal server error', + error: error.message, + }); + } + } + + async getAllServicesWithCategories(req: Request, res: Response): Promise { + try { + const services = await adminService.getAllServicesWithCategories(); + + res.status(200).json({ + success: true, + message: 'Services with categories fetched successfully', + data: services, + count: services.length, + }); + } catch (error: any) { + console.error('Get all services with categories error:', error); + res.status(500).json({ + success: false, + message: 'Internal server error', + error: error.message, + }); + } + } +} + +export const adminController = new AdminController(); diff --git a/src/Admin/middlewares/admin.middleware.ts b/src/Admin/middlewares/admin.middleware.ts new file mode 100644 index 0000000..f3b7afd --- /dev/null +++ b/src/Admin/middlewares/admin.middleware.ts @@ -0,0 +1,120 @@ +import type { Request, Response, NextFunction } from 'express'; +import jwt from 'jsonwebtoken'; +import { adminService } from '../services/admin.service.js'; + +const JWT_SECRET = process.env.JWT_SECRET || 'fallback-secret-key'; + +interface AdminTokenPayload { + adminId: number; + username: string; + role: string; +} + +// Extend Request interface to include admin property +declare global { + namespace Express { + interface Request { + admin?: { + id: number; + username: string; + firstName: string; + lastName: string; + }; + } + } +} + +export const adminAuthMiddleware = async ( + req: Request, + res: Response, + next: NextFunction +): Promise => { + try { + const authHeader = req.headers.authorization; + + if (!authHeader || !authHeader.startsWith('Bearer ')) { + res.status(401).json({ + success: false, + message: 'Access token is required', + }); + return; + } + + const token = authHeader.substring(7); // Remove 'Bearer ' prefix + + try { + const decoded = jwt.verify(token, JWT_SECRET) as AdminTokenPayload; + + // Verify the admin still exists + const admin = await adminService.getAdminById(decoded.adminId); + if (!admin) { + res.status(401).json({ + success: false, + message: 'Admin not found', + }); + return; + } + + // Verify role is admin + if (decoded.role !== 'ADMIN') { + res.status(403).json({ + success: false, + message: 'Admin access required', + }); + return; + } + + // Add admin to request object + req.admin = admin; + next(); + } catch (jwtError) { + res.status(401).json({ + success: false, + message: 'Invalid or expired token', + }); + return; + } + } catch (error: any) { + console.error('Admin auth middleware error:', error); + res.status(500).json({ + success: false, + message: 'Internal server error', + error: error.message, + }); + } +}; + +export const adminOptionalMiddleware = async ( + req: Request, + res: Response, + next: NextFunction +): Promise => { + try { + const authHeader = req.headers.authorization; + + if (!authHeader || !authHeader.startsWith('Bearer ')) { + next(); + return; + } + + const token = authHeader.substring(7); + + try { + const decoded = jwt.verify(token, JWT_SECRET) as AdminTokenPayload; + + if (decoded.role === 'ADMIN') { + const admin = await adminService.getAdminById(decoded.adminId); + if (admin) { + req.admin = admin; + } + } + } catch (jwtError) { + // Token is invalid, but we continue without authentication + } + + next(); + } catch (error: any) { + console.error('Admin optional middleware error:', error); + next(); + } +}; diff --git a/src/Admin/routes/admin.route.ts b/src/Admin/routes/admin.route.ts new file mode 100644 index 0000000..e85dccd --- /dev/null +++ b/src/Admin/routes/admin.route.ts @@ -0,0 +1,22 @@ +import { Router } from 'express'; +import { adminController } from '../controllers/admin.controller.js'; +import { adminAuthMiddleware } from '../middlewares/admin.middleware.js'; +import { validateAdminLogin, validateAdminRegistration, validateAdminUpdate, validateServiceProviderVerification } from '../validators/admin.validator.js'; + +const router = Router(); + +// Public routes +router.post('/register', validateAdminRegistration, adminController.register); +router.post('/login', validateAdminLogin, adminController.login); + +// Protected routes (require admin authentication) +router.get('/profile', adminAuthMiddleware, adminController.getProfile); +router.put('/profile', adminAuthMiddleware, validateAdminUpdate, adminController.updateProfile); +router.get('/all', adminAuthMiddleware, adminController.getAllAdmins); +router.get('/service-providers', adminAuthMiddleware, adminController.getAllServiceProviders); +router.get('/services', adminAuthMiddleware, adminController.getAllServicesWithCategories); +router.get('/customers/count', adminAuthMiddleware, adminController.getCustomerCount); +router.get('/customers', adminAuthMiddleware, adminController.getAllCustomers); +router.put('/service-providers/:providerId/verification', adminAuthMiddleware, validateServiceProviderVerification, adminController.updateServiceProviderVerification); + +export default router; diff --git a/src/Admin/services/admin.service.ts b/src/Admin/services/admin.service.ts new file mode 100644 index 0000000..9cc48aa --- /dev/null +++ b/src/Admin/services/admin.service.ts @@ -0,0 +1,361 @@ +import { prisma } from '../../utils/database.js'; +import { hashPassword, comparePassword } from '../../utils/hash.js'; +import type { Admin } from '@prisma/client'; + +export interface CreateAdminData { + username: string; + password: string; + firstName: string; + lastName: string; +} + +export interface LoginAdminData { + username: string; + password: string; +} + +export interface UpdateAdminData { + username?: string; + password?: string; + firstName?: string; + lastName?: string; +} + +export class AdminService { + async createAdmin(data: CreateAdminData): Promise> { + const hashedPassword = await hashPassword(data.password); + + const admin = await prisma.admin.create({ + data: { + username: data.username, + password: hashedPassword, + firstName: data.firstName, + lastName: data.lastName, + }, + select: { + id: true, + username: true, + firstName: true, + lastName: true, + }, + }); + + return admin; + } + + async loginAdmin(data: LoginAdminData): Promise | null> { + const admin = await prisma.admin.findUnique({ + where: { username: data.username }, + }); + + if (!admin) { + return null; + } + + const isPasswordValid = await comparePassword(data.password, admin.password); + if (!isPasswordValid) { + return null; + } + + return { + id: admin.id, + username: admin.username, + firstName: admin.firstName, + lastName: admin.lastName, + }; + } + + async getAdminById(id: number): Promise | null> { + const admin = await prisma.admin.findUnique({ + where: { id }, + select: { + id: true, + username: true, + firstName: true, + lastName: true, + }, + }); + + return admin; + } + + async getAdminByUsername(username: string): Promise | null> { + const admin = await prisma.admin.findUnique({ + where: { username }, + select: { + id: true, + username: true, + firstName: true, + lastName: true, + }, + }); + + return admin; + } + + async getAllAdmins(): Promise[]> { + const admins = await prisma.admin.findMany({ + select: { + id: true, + username: true, + firstName: true, + lastName: true, + }, + }); + + return admins; + } + + async updateAdmin(id: number, data: UpdateAdminData): Promise | null> { + // Check if username is being updated and if it already exists + if (data.username) { + const existingAdmin = await prisma.admin.findUnique({ + where: { username: data.username }, + }); + + if (existingAdmin && existingAdmin.id !== id) { + throw new Error('Username already exists'); + } + } + + const updateData: any = {}; + + if (data.username) updateData.username = data.username; + if (data.firstName) updateData.firstName = data.firstName; + if (data.lastName) updateData.lastName = data.lastName; + if (data.password) { + updateData.password = await hashPassword(data.password); + } + + const admin = await prisma.admin.update({ + where: { id }, + data: updateData, + select: { + id: true, + username: true, + firstName: true, + lastName: true, + }, + }); + + return admin; + } + + async getAllServiceProvidersWithDetails() { + const serviceProviders = await prisma.serviceProvider.findMany({ + include: { + user: { + select: { + id: true, + email: true, + firstName: true, + lastName: true, + phone: true, + imageUrl: true, + location: true, + address: true, + isEmailVerified: true, + isActive: true, + createdAt: true, + updatedAt: true, + lastLoginAt: true, + socialmedia: true, + }, + }, + companies: true, + services: { + include: { + category: { + select: { + id: true, + name: true, + slug: true, + }, + }, + }, + }, + _count: { + select: { + services: true, + schedules: true, + payments: true, + }, + }, + }, + orderBy: { + createdAt: 'desc', + }, + }); + + return serviceProviders; + } + + async updateServiceProviderVerification(providerId: string, isVerified: boolean): Promise { + // First check if the service provider exists + const existingProvider = await prisma.serviceProvider.findUnique({ + where: { id: providerId }, + include: { + user: { + select: { + id: true, + email: true, + firstName: true, + lastName: true, + }, + }, + }, + }); + + if (!existingProvider) { + throw new Error('Service provider not found'); + } + + // Update the verification status + const updatedProvider = await prisma.serviceProvider.update({ + where: { id: providerId }, + data: { isVerified }, + include: { + user: { + select: { + id: true, + email: true, + firstName: true, + lastName: true, + phone: true, + imageUrl: true, + location: true, + address: true, + isEmailVerified: true, + isActive: true, + createdAt: true, + updatedAt: true, + lastLoginAt: true, + socialmedia: true, + }, + }, + companies: true, + services: { + include: { + category: { + select: { + id: true, + name: true, + slug: true, + }, + }, + }, + }, + _count: { + select: { + services: true, + schedules: true, + payments: true, + }, + }, + }, + }); + + return updatedProvider; + } + + async getCustomerCount(): Promise { + const customerCount = await prisma.user.count({ + where: { + role: 'USER', + isActive: true, + }, + }); + + return customerCount; + } + + async getAllCustomers() { + const customers = await prisma.user.findMany({ + where: { + role: 'USER', + }, + select: { + id: true, + email: true, + firstName: true, + lastName: true, + phone: true, + imageUrl: true, + location: true, + address: true, + isEmailVerified: true, + isActive: true, + createdAt: true, + updatedAt: true, + lastLoginAt: true, + socialmedia: true, + _count: { + select: { + payments: true, + schedules: true, + customerReviewsWritten: true, + customerReviewsReceived: true, + writtenServiceReviews: true, + }, + }, + }, + orderBy: { + createdAt: 'desc', + }, + }); + + return customers; + } + + async getAllServicesWithCategories() { + const services = await prisma.service.findMany({ + include: { + category: { + select: { + id: true, + name: true, + slug: true, + description: true, + parentId: true, + parent: { + select: { + id: true, + name: true, + slug: true, + }, + }, + }, + }, + provider: { + include: { + user: { + select: { + id: true, + email: true, + firstName: true, + lastName: true, + phone: true, + location: true, + isActive: true, + }, + }, + }, + }, + _count: { + select: { + schedules: true, + payments: true, + serviceReviews: true, + }, + }, + }, + orderBy: { + createdAt: 'desc', + }, + }); + + return services; + } +} + +export const adminService = new AdminService(); diff --git a/src/Admin/validators/admin.validator.ts b/src/Admin/validators/admin.validator.ts new file mode 100644 index 0000000..e05d7e0 --- /dev/null +++ b/src/Admin/validators/admin.validator.ts @@ -0,0 +1,230 @@ +import Joi from 'joi'; +import type { Request, Response, NextFunction } from 'express'; + +const adminRegistrationSchema = Joi.object({ + username: Joi.string() + .alphanum() + .min(3) + .max(30) + .required() + .messages({ + 'string.alphanum': 'Username must only contain alphanumeric characters', + 'string.min': 'Username must be at least 3 characters long', + 'string.max': 'Username must not exceed 30 characters', + 'any.required': 'Username is required', + }), + password: Joi.string() + .min(8) + .max(128) + .pattern(new RegExp('^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]')) + .required() + .messages({ + 'string.min': 'Password must be at least 8 characters long', + 'string.max': 'Password must not exceed 128 characters', + 'string.pattern.base': 'Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character', + 'any.required': 'Password is required', + }), + firstName: Joi.string() + .min(1) + .max(50) + .pattern(new RegExp('^[a-zA-Z\\s]+$')) + .required() + .messages({ + 'string.min': 'First name must be at least 1 character long', + 'string.max': 'First name must not exceed 50 characters', + 'string.pattern.base': 'First name must only contain letters and spaces', + 'any.required': 'First name is required', + }), + lastName: Joi.string() + .min(1) + .max(50) + .pattern(new RegExp('^[a-zA-Z\\s]+$')) + .required() + .messages({ + 'string.min': 'Last name must be at least 1 character long', + 'string.max': 'Last name must not exceed 50 characters', + 'string.pattern.base': 'Last name must only contain letters and spaces', + 'any.required': 'Last name is required', + }), +}); + +const adminLoginSchema = Joi.object({ + username: Joi.string() + .required() + .messages({ + 'any.required': 'Username is required', + }), + password: Joi.string() + .required() + .messages({ + 'any.required': 'Password is required', + }), +}); + +const adminUpdateSchema = Joi.object({ + username: Joi.string() + .alphanum() + .min(3) + .max(30) + .optional() + .messages({ + 'string.alphanum': 'Username must only contain alphanumeric characters', + 'string.min': 'Username must be at least 3 characters long', + 'string.max': 'Username must not exceed 30 characters', + }), + password: Joi.string() + .min(8) + .max(128) + .pattern(new RegExp('^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]')) + .optional() + .messages({ + 'string.min': 'Password must be at least 8 characters long', + 'string.max': 'Password must not exceed 128 characters', + 'string.pattern.base': 'Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character', + }), + firstName: Joi.string() + .min(1) + .max(50) + .pattern(new RegExp('^[a-zA-Z\\s]+$')) + .optional() + .messages({ + 'string.min': 'First name must be at least 1 character long', + 'string.max': 'First name must not exceed 50 characters', + 'string.pattern.base': 'First name must only contain letters and spaces', + }), + lastName: Joi.string() + .min(1) + .max(50) + .pattern(new RegExp('^[a-zA-Z\\s]+$')) + .optional() + .messages({ + 'string.min': 'Last name must be at least 1 character long', + 'string.max': 'Last name must not exceed 50 characters', + 'string.pattern.base': 'Last name must only contain letters and spaces', + }), +}).min(1).messages({ + 'object.min': 'At least one field must be provided for update', +}); + +export const validateAdminRegistration = ( + req: Request, + res: Response, + next: NextFunction +): void => { + const { error } = adminRegistrationSchema.validate(req.body, { + abortEarly: false, + }); + + if (error) { + const errors = error.details.map((detail) => ({ + field: detail.path.join('.'), + message: detail.message, + })); + + res.status(400).json({ + success: false, + message: 'Validation failed', + errors, + }); + return; + } + + next(); +}; + +export const validateAdminLogin = ( + req: Request, + res: Response, + next: NextFunction +): void => { + const { error } = adminLoginSchema.validate(req.body, { + abortEarly: false, + }); + + if (error) { + const errors = error.details.map((detail) => ({ + field: detail.path.join('.'), + message: detail.message, + })); + + res.status(400).json({ + success: false, + message: 'Validation failed', + errors, + }); + return; + } + + next(); +}; + +export const validateAdminUpdate = ( + req: Request, + res: Response, + next: NextFunction +): void => { + const { error } = adminUpdateSchema.validate(req.body, { + abortEarly: false, + }); + + if (error) { + const errors = error.details.map((detail) => ({ + field: detail.path.join('.'), + message: detail.message, + })); + + res.status(400).json({ + success: false, + message: 'Validation failed', + errors, + }); + return; + } + + next(); +}; + +const serviceProviderVerificationSchema = Joi.object({ + isVerified: Joi.boolean() + .required() + .messages({ + 'boolean.base': 'isVerified must be a boolean value', + 'any.required': 'isVerified is required', + }), +}); + +export const validateServiceProviderVerification = ( + req: Request, + res: Response, + next: NextFunction +): void => { + const { error } = serviceProviderVerificationSchema.validate(req.body, { + abortEarly: false, + }); + + if (error) { + const errors = error.details.map((detail) => ({ + field: detail.path.join('.'), + message: detail.message, + })); + + res.status(400).json({ + success: false, + message: 'Validation failed', + errors, + }); + return; + } + + // Validate providerId parameter + const { providerId } = req.params; + if (!providerId || typeof providerId !== 'string' || providerId.trim() === '') { + res.status(400).json({ + success: false, + message: 'Valid provider ID is required', + }); + return; + } + + next(); +}; diff --git a/src/controllers/activity.controller.ts b/src/controllers/activity.controller.ts new file mode 100755 index 0000000..fb01538 --- /dev/null +++ b/src/controllers/activity.controller.ts @@ -0,0 +1,20 @@ +import { Request, Response, NextFunction } from 'express'; +import { getActivities } from '../services/activity.service'; +import { EntityType } from '@prisma/client'; + +export const getActivitiesController = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const userId = (req as any).user.id; + const { teamId, entityType, limit } = req.query; + + const filters: any = {}; + if (teamId) filters.teamId = teamId as string; + if (entityType) filters.entityType = entityType as EntityType; + if (limit) filters.limit = parseInt(limit as string); + + const activities = await getActivities(userId, filters); + res.status(200).json({ activities }); + } catch (error) { + next(error); + } +}; \ No newline at end of file diff --git a/src/controllers/catagory.controller.js b/src/controllers/catagory.controller.js deleted file mode 100644 index ddbf870..0000000 --- a/src/controllers/catagory.controller.js +++ /dev/null @@ -1,226 +0,0 @@ -import * as categoryService from '../services/catagory.service.js'; - -/** - * Create a new category - */ -export const createCategory = async (req, res, next) => { - try { - const categoryData = req.body; - const newCategory = await categoryService.createCategory(categoryData); - - res.status(201).json({ - success: true, - message: 'Category created successfully', - data: newCategory - }); - } catch (error) { - next(error); - } -}; - -/** - * Get all categories with optional filtering - */ -export const getCategories = async (req, res, next) => { - try { - const filters = { - parentId: req.query.parentId, - includeChildren: req.query.includeChildren !== 'false', - includeParent: req.query.includeParent !== 'false', - includeServices: req.query.includeServices === 'true' - }; - - const categories = await categoryService.getAllCategories(filters); - - res.status(200).json({ - success: true, - message: 'Categories retrieved successfully', - data: categories - }); - } catch (error) { - next(error); - } -}; - -/** - * Get category by ID - */ -export const getCategoryById = async (req, res, next) => { - try { - const { id } = req.params; - const options = { - includeChildren: req.query.includeChildren !== 'false', - includeParent: req.query.includeParent !== 'false', - includeServices: req.query.includeServices === 'true' - }; - - const category = await categoryService.getCategoryById(id, options); - - if (!category) { - return res.status(404).json({ - success: false, - message: 'Category not found' - }); - } - - res.status(200).json({ - success: true, - message: 'Category retrieved successfully', - data: category - }); - } catch (error) { - next(error); - } -}; - -/** - * Get category by slug - */ -export const getCategoryBySlug = async (req, res, next) => { - try { - const { slug } = req.params; - const options = { - includeChildren: req.query.includeChildren !== 'false', - includeParent: req.query.includeParent !== 'false', - includeServices: req.query.includeServices === 'true' - }; - - const category = await categoryService.getCategoryBySlug(slug, options); - - if (!category) { - return res.status(404).json({ - success: false, - message: 'Category not found' - }); - } - - res.status(200).json({ - success: true, - message: 'Category retrieved successfully', - data: category - }); - } catch (error) { - next(error); - } -}; - -/** - * Update category - */ -export const updateCategory = async (req, res, next) => { - try { - const { id } = req.params; - const updateData = req.body; - - const updatedCategory = await categoryService.updateCategory(id, updateData); - - res.status(200).json({ - success: true, - message: 'Category updated successfully', - data: updatedCategory - }); - } catch (error) { - next(error); - } -}; - -/** - * Delete category - */ -export const deleteCategory = async (req, res, next) => { - try { - const { id } = req.params; - const options = { - force: req.query.force === 'true' - }; - - const deletedCategory = await categoryService.deleteCategory(id, options); - - res.status(200).json({ - success: true, - message: 'Category deleted successfully', - data: deletedCategory - }); - } catch (error) { - next(error); - } -}; - -/** - * Get root categories (categories with no parent) - */ -export const getRootCategories = async (req, res, next) => { - try { - const options = { - includeChildren: req.query.includeChildren !== 'false' - }; - - const rootCategories = await categoryService.getRootCategories(options); - - res.status(200).json({ - success: true, - message: 'Root categories retrieved successfully', - data: rootCategories - }); - } catch (error) { - next(error); - } -}; - -/** - * Get category hierarchy - */ -export const getCategoryHierarchy = async (req, res, next) => { - try { - const { id } = req.params; - - const hierarchy = await categoryService.getCategoryHierarchy(id); - - if (!hierarchy) { - return res.status(404).json({ - success: false, - message: 'Category not found' - }); - } - - res.status(200).json({ - success: true, - message: 'Category hierarchy retrieved successfully', - data: hierarchy - }); - } catch (error) { - next(error); - } -}; - -/** - * Search categories - */ -export const searchCategories = async (req, res, next) => { - try { - const { q: searchTerm } = req.query; - - if (!searchTerm) { - return res.status(400).json({ - success: false, - message: 'Search term is required' - }); - } - - const options = { - includeChildren: req.query.includeChildren !== 'false', - includeParent: req.query.includeParent !== 'false' - }; - - const categories = await categoryService.searchCategories(searchTerm, options); - - res.status(200).json({ - success: true, - message: 'Search completed successfully', - data: categories, - searchTerm - }); - } catch (error) { - next(error); - } -}; diff --git a/src/controllers/comment.controller.ts b/src/controllers/comment.controller.ts new file mode 100755 index 0000000..aba75c8 --- /dev/null +++ b/src/controllers/comment.controller.ts @@ -0,0 +1,69 @@ +import { Request, Response, NextFunction } from 'express'; +import { + createComment, + getTaskComments, + updateComment, + deleteComment +} from '../services/comment.service'; + +export const createCommentController = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const userId = (req as any).user.id; + const comment = await createComment(userId, req.body); + res.status(201).json({ message: 'Comment created successfully', comment }); + } catch (error) { + next(error); + } +}; + +export const getTaskCommentsController = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const userId = (req as any).user.id; + const { taskId } = req.params; + const { page = '1', limit = '10' } = req.query; + + if (!taskId) { + res.status(400).json({ message: 'Task ID is required' }); + return; + } + + const result = await getTaskComments(taskId, userId, parseInt(page as string), parseInt(limit as string)); + res.status(200).json(result); + } catch (error) { + next(error); + } +}; + +export const updateCommentController = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const userId = (req as any).user.id; + const { commentId } = req.params; + + if (!commentId) { + res.status(400).json({ message: 'Comment ID is required' }); + return; + } + + const comment = await updateComment(commentId, userId, req.body); + res.status(200).json({ message: 'Comment updated successfully', comment }); + } catch (error) { + next(error); + } +}; + +export const deleteCommentController = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const userId = (req as any).user.id; + const { commentId } = req.params; + + if (!commentId) { + res.status(400).json({ message: 'Comment ID is required' }); + return; + } + + const result = await deleteComment(commentId, userId); + res.status(200).json(result); + } catch (error) { + next(error); + } +}; \ No newline at end of file diff --git a/src/controllers/company.controller.js b/src/controllers/company.controller.js deleted file mode 100644 index ed59e28..0000000 --- a/src/controllers/company.controller.js +++ /dev/null @@ -1,45 +0,0 @@ -import * as companyService from '../services/company.service.js'; - -export const createCompany = async (req, res, next) => { - try { - const company = await companyService.createCompany(req.user.id, req.body); - res.status(201).json({ - message: 'Company created successfully', - company - }); - } catch (err) { - next(err); - } -}; - -export const updateCompany = async (req, res, next) => { - try { - const company = await companyService.updateCompany(req.user.id, req.params.companyId, req.body); - res.status(200).json({ - message: 'Company updated successfully', - company - }); - } catch (err) { - next(err); - } -}; - -export const deleteCompany = async (req, res, next) => { - try { - await companyService.deleteCompany(req.user.id, req.params.companyId); - res.status(200).json({ - message: 'Company deleted successfully' - }); - } catch (err) { - next(err); - } -}; - -export const getCompanies = async (req, res, next) => { - try { - const companies = await companyService.getCompanies(req.user.id); - res.status(200).json(companies); - } catch (err) { - next(err); - } -}; diff --git a/src/controllers/dependency.controller.ts b/src/controllers/dependency.controller.ts new file mode 100755 index 0000000..b18737f --- /dev/null +++ b/src/controllers/dependency.controller.ts @@ -0,0 +1,81 @@ +import { Request, Response, NextFunction } from 'express'; +import { + addTaskDependency, + removeTaskDependency, + getTaskDependencies, + validateTaskCanBeCompleted, + getDependencyGraph +} from '../services/dependency.service'; + +export const addTaskDependencyController = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const userId = (req as any).user.id; + const dependency = await addTaskDependency(userId, req.body); + res.status(201).json({ message: 'Dependency added successfully', dependency }); + } catch (error) { + next(error); + } +}; + +export const removeTaskDependencyController = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const userId = (req as any).user.id; + const { dependencyId } = req.params; + + if (!dependencyId) { + res.status(400).json({ message: 'Dependency ID is required' }); + return; + } + + const result = await removeTaskDependency(userId, dependencyId); + res.status(200).json(result); + } catch (error) { + next(error); + } +}; + +export const getTaskDependenciesController = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const userId = (req as any).user.id; + const { taskId } = req.params; + + if (!taskId) { + res.status(400).json({ message: 'Task ID is required' }); + return; + } + + const dependencies = await getTaskDependencies(taskId, userId); + res.status(200).json(dependencies); + } catch (error) { + next(error); + } +}; + +export const validateTaskCompletionController = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const userId = (req as any).user.id; + const { taskId } = req.params; + + if (!taskId) { + res.status(400).json({ message: 'Task ID is required' }); + return; + } + + const validation = await validateTaskCanBeCompleted(taskId, userId); + res.status(200).json(validation); + } catch (error) { + next(error); + } +}; + +export const getDependencyGraphController = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const userId = (req as any).user.id; + const { teamId } = req.query; + + const graph = await getDependencyGraph(userId, teamId as string); + res.status(200).json(graph); + } catch (error) { + next(error); + } +}; \ No newline at end of file diff --git a/src/controllers/provider.controller.js b/src/controllers/provider.controller.js deleted file mode 100644 index 2760dd2..0000000 --- a/src/controllers/provider.controller.js +++ /dev/null @@ -1,43 +0,0 @@ -import * as providerService from '../services/provider.service.js'; - -export const createProvider = async (req, res, next) => { - try { - const provider = await providerService.createProvider(req.user.id, req.body); - res.status(201).json({ - message: 'Service provider profile created successfully', - provider - }); - } catch (err) { - next(err); - } -}; - -export const updateProvider = async (req, res, next) => { - try { - const provider = await providerService.updateProvider(req.user.id, req.body); - res.status(200).json({ - message: 'Service provider profile updated successfully', - provider - }); - } catch (err) { - next(err); - } -}; - -export const deleteProvider = async (req, res, next) => { - try { - const result = await providerService.deleteProvider(req.user.id); - res.status(200).json(result); - } catch (err) { - next(err); - } -}; - -export const getProviderProfile = async (req, res, next) => { - try { - const provider = await providerService.getProviderProfile(req.user.id); - res.status(200).json(provider); - } catch (err) { - next(err); - } -}; diff --git a/src/controllers/services.controller.js b/src/controllers/services.controller.js deleted file mode 100644 index 88884a2..0000000 --- a/src/controllers/services.controller.js +++ /dev/null @@ -1,111 +0,0 @@ -import * as serviceService from '../services/services.service.js'; - -/** - * Create a new service - */ -export const createService = async (req, res, next) => { - try { - const serviceData = req.body; - const newService = await serviceService.createService(serviceData); - - res.status(201).json({ - success: true, - message: 'Service created successfully', - data: newService - }); - } catch (error) { - next(error); - } -}; - -/** - * Get all services with optional filtering - */ -export const getServices = async (req, res, next) => { - try { - const filters = { - providerId: req.query.providerId, - categoryId: req.query.categoryId, - isActive: req.query.isActive ? req.query.isActive === 'true' : undefined, - skip: req.query.skip ? parseInt(req.query.skip) : 0, - take: req.query.take ? parseInt(req.query.take) : 10 - }; - - const services = await serviceService.getServices(filters); - - res.status(200).json({ - success: true, - message: 'Services retrieved successfully', - data: services, - pagination: { - skip: filters.skip, - take: filters.take - } - }); - } catch (error) { - next(error); - } -}; - -/** - * Get a single service by ID - */ -export const getServiceById = async (req, res, next) => { - try { - const { id } = req.params; - const service = await serviceService.getServiceById(id); - - if (!service) { - return res.status(404).json({ - success: false, - message: 'Service not found' - }); - } - - res.status(200).json({ - success: true, - message: 'Service retrieved successfully', - data: service - }); - } catch (error) { - next(error); - } -}; - -/** - * Update a service - */ -export const updateService = async (req, res, next) => { - try { - const { id } = req.params; - const updateData = req.body; - - const updatedService = await serviceService.updateService(id, updateData); - - res.status(200).json({ - success: true, - message: 'Service updated successfully', - data: updatedService - }); - } catch (error) { - next(error); - } -}; - -/** - * Delete a service - */ -export const deleteService = async (req, res, next) => { - try { - const { id } = req.params; - - await serviceService.deleteService(id); - - res.status(200).json({ - success: true, - message: 'Service deleted successfully' - }); - } catch (error) { - next(error); - } -}; diff --git a/src/controllers/services.controller.ts b/src/controllers/services.controller.ts new file mode 100755 index 0000000..24137e2 --- /dev/null +++ b/src/controllers/services.controller.ts @@ -0,0 +1,684 @@ +import type { Request, Response, NextFunction } from 'express'; +import * as serviceService from '../services/services.service.js'; +import { semanticSearchService } from '../services/semantic-search.service.js'; +import googleMapsService from '../services/googleMaps.service.js'; + +/** + * Create a new service + */ +export const createService = async (req: Request, res: Response, next: NextFunction) => { + try { + console.log('=== SERVICE CREATION DEBUG ==='); + console.log('Request body received:', JSON.stringify(req.body, null, 2)); + + const serviceData = req.body; + + // Handle location data if provided + if (serviceData.address || (serviceData.latitude && serviceData.longitude)) { + try { + let locationData; + + if (serviceData.latitude && serviceData.longitude) { + // Manual coordinates provided - validate and reverse geocode + if (!googleMapsService.validateCoordinates(serviceData.latitude, serviceData.longitude)) { + return res.status(400).json({ + success: false, + message: 'Invalid coordinates provided' + }); + } + + locationData = await googleMapsService.reverseGeocode( + serviceData.latitude, + serviceData.longitude + ); + } else if (serviceData.address) { + // Address provided - geocode to get coordinates + locationData = await googleMapsService.geocodeAddress(serviceData.address); + } + + if (locationData) { + // Merge location data into service data + serviceData.latitude = locationData.lat; + serviceData.longitude = locationData.lng; + serviceData.address = locationData.formatted_address; + serviceData.city = locationData.city; + serviceData.state = locationData.state; + serviceData.country = locationData.country; + serviceData.postalCode = locationData.postal_code; + serviceData.locationLastUpdated = new Date(); + } + } catch (locationError) { + console.warn('Location processing failed:', locationError); + // Continue without location data rather than failing the entire request + } + } + + // Debug: Check if videoUrl is present + console.log('Video URL in service data:', serviceData.videoUrl); + console.log('Video URL type:', typeof serviceData.videoUrl); + console.log('Video URL length:', serviceData.videoUrl ? serviceData.videoUrl.length : 'N/A'); + + console.log('Calling service creation...'); + const newService = await serviceService.createService(serviceData); + + console.log('Service created successfully. Result:', JSON.stringify(newService, null, 2)); + console.log('Video URL in created service:', (newService as any).videoUrl); + + res.status(201).json({ + success: true, + message: 'Service created successfully', + data: newService + }); + } catch (error) { + console.error('=== SERVICE CREATION ERROR ==='); + console.error('Error details:', error); + console.error('Error message:', error instanceof Error ? error.message : 'Unknown error'); + console.error('Error stack:', error instanceof Error ? error.stack : 'No stack trace'); + next(error); + } +}; + +/** + * Get all services with optional filtering + */ +export const getServices = async (req: Request, res: Response, next: NextFunction) => { + try { + const filters: any = { + providerId: req.query.providerId as string, + categoryId: req.query.categoryId as string, + skip: req.query.skip ? parseInt(req.query.skip as string) : 0, + take: req.query.take ? parseInt(req.query.take as string) : 10 + }; + if (typeof req.query.isActive !== 'undefined') { + filters.isActive = req.query.isActive === 'true'; + } + + const services = await serviceService.getServices(filters); + + res.status(200).json({ + success: true, + message: 'Services retrieved successfully', + data: services, + pagination: { + skip: filters.skip, + take: filters.take + } + }); + } catch (error) { + next(error); + } +}; + +/** + * Get a single service by ID + */ +export const getServiceById = async (req: Request, res: Response, next: NextFunction) => { + try { + const { id } = req.params; + const service = await serviceService.getServiceById(id!); + + if (!service) { + return res.status(404).json({ + success: false, + message: 'Service not found' + }); + } + + res.status(200).json({ + success: true, + message: 'Service retrieved successfully', + data: service + }); + } catch (error) { + next(error); + } +}; + +/** + * Update a service + */ +export const updateService = async (req: Request, res: Response, next: NextFunction) => { + try { + const { id } = req.params; + const updateData = req.body; + + const updatedService = await serviceService.updateService(id!, updateData); + + res.status(200).json({ + success: true, + message: 'Service updated successfully', + data: updatedService + }); + } catch (error) { + next(error); + } +}; + +/** + * Delete a service + */ +export const deleteService = async (req: Request, res: Response, next: NextFunction) => { + try { + const { id } = req.params; + + await serviceService.deleteService(id!); + + res.status(200).json({ + success: true, + message: 'Service deleted successfully' + }); + } catch (error) { + next(error); + } +}; + +/** + * Get a service by conversation ID + */ +export const getServiceByConversationId = async (req: Request, res: Response, next: NextFunction) => { + try { + const { conversationId } = req.params; + const service = await serviceService.getServiceByConversationId(conversationId!); + + if (!service) { + return res.status(404).json({ + success: false, + message: 'Service not found for this conversation' + }); + } + + res.status(200).json({ + success: true, + message: 'Service retrieved successfully', + data: service + }); + } catch (error) { + next(error); + } +}; + +/** + * Hybrid search for services (combines semantic search with geolocation) + */ +export const hybridSearchServices = async (req: Request, res: Response, next: NextFunction) => { + try { + const { + query, + limit = 20, + threshold = 0.3, + categoryId, + providerId, + minPrice, + maxPrice, + // Location parameters + lat, + lng, + latitude, + longitude, + address, + radius = 50, // Default 50km radius + includeWithoutLocation = true + } = req.query; + + // Determine user coordinates + let userLat: number | undefined, userLng: number | undefined; + let locationProvided = false; + + if (lat && lng) { + userLat = parseFloat(lat as string); + userLng = parseFloat(lng as string); + locationProvided = true; + } else if (latitude && longitude) { + userLat = parseFloat(latitude as string); + userLng = parseFloat(longitude as string); + locationProvided = true; + } else if (address) { + try { + const locationData = await googleMapsService.geocodeAddress(address as string); + userLat = locationData.lat; + userLng = locationData.lng; + locationProvided = true; + } catch (error) { + console.warn('Geocoding failed for address:', address, error); + // Continue without location filtering + } + } + + let results: any[] = []; + let searchType = ''; + + // Case 1: Both query and location provided - Semantic search with location filtering + if (query && typeof query === 'string' && query.trim() && locationProvided && userLat && userLng) { + console.log('Hybrid search: semantic + location'); + searchType = 'hybrid'; + + // First get semantic search results + const semanticResults = await semanticSearchService.searchServices({ + query: query as string, + limit: parseInt(limit as string) * 2, // Get more results to filter by location + threshold: parseFloat(threshold as string), + categoryId: categoryId as string, + providerId: providerId as string, + minPrice: minPrice ? parseFloat(minPrice as string) : undefined, + maxPrice: maxPrice ? parseFloat(maxPrice as string) : undefined, + }); + + // Then filter by location and add distance + const locationFilteredResults = []; + + for (const service of semanticResults) { + // Get full service data with location + const fullService = await serviceService.getServiceById(service.id); + + if (fullService && fullService.latitude && fullService.longitude) { + // Calculate distance + const distance = googleMapsService.calculateDistance( + userLat!, + userLng!, + fullService.latitude, + fullService.longitude + ); + + // Check if within radius + if (distance <= parseFloat(radius as string)) { + locationFilteredResults.push({ + ...service, + latitude: fullService.latitude, + longitude: fullService.longitude, + address: fullService.address, + city: fullService.city, + state: fullService.state, + country: fullService.country, + postalCode: fullService.postalCode, + serviceRadiusKm: fullService.serviceRadiusKm, + distance_km: distance + }); + } + } else if (includeWithoutLocation === 'true' || includeWithoutLocation === true) { + // Include services without location (available everywhere) + locationFilteredResults.push({ + ...service, + distance_km: null // No distance for services without location + }); + } + } + + // Sort by similarity first, then by distance + results = locationFilteredResults + .sort((a, b) => { + if (a.similarity !== b.similarity) { + return b.similarity - a.similarity; // Higher similarity first + } + if (a.distance_km !== null && b.distance_km !== null) { + return a.distance_km - b.distance_km; // Closer distance first + } + if (a.distance_km === null) return 1; // Services without location go last + if (b.distance_km === null) return -1; + return 0; + }) + .slice(0, parseInt(limit as string)); + } + + // Case 2: Only query provided - Pure semantic search + else if (query && typeof query === 'string' && query.trim()) { + console.log('Semantic search only'); + searchType = 'semantic'; + + results = await semanticSearchService.searchServices({ + query: query as string, + limit: parseInt(limit as string), + threshold: parseFloat(threshold as string), + categoryId: categoryId as string, + providerId: providerId as string, + minPrice: minPrice ? parseFloat(minPrice as string) : undefined, + maxPrice: maxPrice ? parseFloat(maxPrice as string) : undefined, + }); + } + + // Case 3: Only location provided - Location-based search + else if (locationProvided && userLat && userLng) { + console.log('Location search only'); + searchType = 'location'; + + const locationSearchOptions = { + latitude: userLat, + longitude: userLng, + radius: parseFloat(radius as string), + page: 1, + limit: parseInt(limit as string), + categoryId: categoryId as string, + minPrice: minPrice ? parseFloat(minPrice as string) : undefined, + maxPrice: maxPrice ? parseFloat(maxPrice as string) : undefined + }; + + const locationResults = await serviceService.searchServicesByLocation(locationSearchOptions); + results = locationResults.services; + } + + // Case 4: No query and no location - Return general service list + else { + console.log('General service listing'); + searchType = 'general'; + + const generalResults = await serviceService.getServices({ + categoryId: categoryId as string, + providerId: providerId as string, + isActive: true, + skip: 0, + take: parseInt(limit as string) + }); + + results = generalResults; + } + + res.status(200).json({ + success: true, + message: 'Search completed successfully', + data: { + query: query || null, + location: locationProvided ? { latitude: userLat, longitude: userLng, radius } : null, + searchType, + results: results, + count: results.length + } + }); + } catch (error) { + console.error('Hybrid search error:', error); + next(error); + } +}; + +/** + * Semantic search for services + */ +export const searchServices = async (req: Request, res: Response, next: NextFunction) => { + try { + const { query, limit, threshold, categoryId, providerId, minPrice, maxPrice } = req.query; + + if (!query || typeof query !== 'string') { + return res.status(400).json({ + success: false, + message: 'Search query is required' + }); + } + + const searchOptions = { + query: query as string, + limit: limit ? parseInt(limit as string) : 20, + threshold: threshold ? parseFloat(threshold as string) : 0.7, + categoryId: categoryId as string, + providerId: providerId as string, + minPrice: minPrice ? parseFloat(minPrice as string) : undefined, + maxPrice: maxPrice ? parseFloat(maxPrice as string) : undefined, + }; + + const results = await semanticSearchService.searchServices(searchOptions); + + res.status(200).json({ + success: true, + message: 'Semantic search completed successfully', + data: { + query: query, + results: results, + count: results.length + } + }); + } catch (error) { + next(error); + } +}; + +/** + * Find similar services to a given service + */ +export const getSimilarServices = async (req: Request, res: Response, next: NextFunction) => { + try { + const { id } = req.params; + const { limit } = req.query; + + const similarServices = await semanticSearchService.findSimilarServices( + id!, + limit ? parseInt(limit as string) : 5 + ); + + res.status(200).json({ + success: true, + message: 'Similar services retrieved successfully', + data: similarServices + }); + } catch (error) { + next(error); + } +}; + +/** + * Update embeddings for a specific service + */ +export const updateServiceEmbeddings = async (req: Request, res: Response, next: NextFunction) => { + try { + const { id } = req.params; + + await semanticSearchService.updateServiceEmbeddings(id!); + + res.status(200).json({ + success: true, + message: 'Service embeddings updated successfully' + }); + } catch (error) { + next(error); + } +}; + +/** + * Batch update embeddings for all services + */ +export const updateAllServiceEmbeddings = async (req: Request, res: Response, next: NextFunction) => { + try { + const { batchSize } = req.query; + + const updatedCount = await semanticSearchService.updateAllServiceEmbeddings( + batchSize ? parseInt(batchSize as string) : 10 + ); + + res.status(200).json({ + success: true, + message: `Batch embedding update completed. Updated ${updatedCount} services.`, + data: { + updatedCount + } + }); + } catch (error) { + next(error); + } +}; + +/** + * Search services by location + */ +export const searchServicesByLocation = async (req: Request, res: Response, next: NextFunction) => { + try { + const { + lat, + lng, + address, + radius = 10, // Default 10km radius + page = 1, + limit = 20, + categoryId, + minPrice, + maxPrice + } = req.query; + + let userLat: number, userLng: number; + + // Determine user coordinates + if (lat && lng) { + userLat = parseFloat(lat as string); + userLng = parseFloat(lng as string); + + if (!googleMapsService.validateCoordinates(userLat, userLng)) { + return res.status(400).json({ + success: false, + message: 'Invalid coordinates provided' + }); + } + } else if (address) { + try { + const locationData = await googleMapsService.geocodeAddress(address as string); + userLat = locationData.lat; + userLng = locationData.lng; + } catch (error) { + return res.status(400).json({ + success: false, + message: 'Could not geocode the provided address' + }); + } + } else { + return res.status(400).json({ + success: false, + message: 'Location required for search (provide lat/lng or address)' + }); + } + + const searchOptions = { + latitude: userLat, + longitude: userLng, + radius: parseFloat(radius as string), + page: parseInt(page as string), + limit: parseInt(limit as string), + categoryId: categoryId as string, + minPrice: minPrice ? parseFloat(minPrice as string) : undefined, + maxPrice: maxPrice ? parseFloat(maxPrice as string) : undefined + }; + + const results = await serviceService.searchServicesByLocation(searchOptions); + + res.status(200).json({ + success: true, + message: 'Location-based search completed successfully', + data: { + services: results.services, + pagination: { + total: results.total, + page: searchOptions.page, + limit: searchOptions.limit, + totalPages: Math.ceil(results.total / searchOptions.limit) + }, + search_location: { + lat: userLat, + lng: userLng, + radius: searchOptions.radius + } + } + }); + } catch (error) { + next(error); + } +}; + +/** + * Get user location from IP address + */ +export const getLocationFromIP = async (req: Request, res: Response, next: NextFunction) => { + try { + const clientIP = req.ip || req.connection.remoteAddress || req.headers['x-forwarded-for'] as string; + + try { + const locationData = await googleMapsService.getLocationFromIP(clientIP); + + res.status(200).json({ + success: true, + message: 'Location detected from IP address', + data: locationData + }); + } catch (error) { + res.status(200).json({ + success: false, + message: 'Could not determine location from IP address', + data: null + }); + } + } catch (error) { + next(error); + } +}; + +/** + * Geocode an address + */ +export const geocodeAddress = async (req: Request, res: Response, next: NextFunction) => { + try { + const { address } = req.body; + + if (!address) { + return res.status(400).json({ + success: false, + message: 'Address is required' + }); + } + + try { + const locationData = await googleMapsService.geocodeAddress(address); + + res.status(200).json({ + success: true, + message: 'Address geocoded successfully', + data: locationData + }); + } catch (error) { + res.status(400).json({ + success: false, + message: 'Could not geocode the provided address' + }); + } + } catch (error) { + next(error); + } +}; + +/** + * Reverse geocode coordinates + */ +export const reverseGeocode = async (req: Request, res: Response, next: NextFunction) => { + try { + const { lat, lng, latitude, longitude } = req.body; + + // Support both lat/lng and latitude/longitude formats + const latValue = lat || latitude; + const lngValue = lng || longitude; + + if (!latValue || !lngValue) { + return res.status(400).json({ + success: false, + message: 'Latitude and longitude are required' + }); + } + + const latParsed = parseFloat(latValue); + const lngParsed = parseFloat(lngValue); + + if (!googleMapsService.validateCoordinates(latParsed, lngParsed)) { + return res.status(400).json({ + success: false, + message: 'Invalid coordinates provided' + }); + } + + try { + const locationData = await googleMapsService.reverseGeocode(latParsed, lngParsed); + + res.status(200).json({ + success: true, + message: 'Coordinates reverse geocoded successfully', + data: locationData + }); + } catch (error) { + res.status(400).json({ + success: false, + message: 'Could not reverse geocode the provided coordinates' + }); + } + } catch (error) { + next(error); + } +}; diff --git a/src/controllers/task.controller.ts b/src/controllers/task.controller.ts new file mode 100755 index 0000000..26df6e7 --- /dev/null +++ b/src/controllers/task.controller.ts @@ -0,0 +1,147 @@ +import { Request, Response, NextFunction } from 'express'; +import { + createTask, + getTasks, + getTaskById, + updateTask, + deleteTask, + assignUsersToTask, + getTaskStatistics, + TaskFilters +} from '../services/task.service'; +import { TaskStatus, TaskPriority } from '@prisma/client'; + +export const createTaskController = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const userId = (req as any).user.id; + console.log('Creating task for user:', userId); // Debug log + console.log('Task data received:', req.body); // Debug log + + const task = await createTask(userId, req.body); + console.log('Task created successfully:', task.id); // Debug log + + res.status(201).json({ message: 'Task created successfully', task }); + } catch (error) { + console.error('Error in createTaskController:', error); // Debug log + next(error); + } +}; + +export const getTasksController = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const userId = (req as any).user.id; + const { + status, + priority, + assigneeId, + teamId, + createdById, + dueDateBefore, + dueDateAfter, + search, + page = '1', + limit = '10' + } = req.query; + + const filters: TaskFilters = {}; + if (status) filters.status = status as TaskStatus; + if (priority) filters.priority = priority as TaskPriority; + if (assigneeId) filters.assigneeId = assigneeId as string; + if (teamId) filters.teamId = teamId as string; + if (createdById) filters.createdById = createdById as string; + if (dueDateBefore) filters.dueDateBefore = new Date(dueDateBefore as string); + if (dueDateAfter) filters.dueDateAfter = new Date(dueDateAfter as string); + if (search) filters.search = search as string; + + const result = await getTasks(userId, filters, parseInt(page as string), parseInt(limit as string)); + res.status(200).json(result); + } catch (error) { + next(error); + } +}; + +export const getTaskByIdController = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const userId = (req as any).user.id; + const { taskId } = req.params; + + if (!taskId) { + res.status(400).json({ message: 'Task ID is required' }); + return; + } + + const task = await getTaskById(taskId, userId); + res.status(200).json({ task }); + } catch (error) { + next(error); + } +}; + +export const updateTaskController = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const userId = (req as any).user.id; + const { taskId } = req.params; + + if (!taskId) { + res.status(400).json({ message: 'Task ID is required' }); + return; + } + + const task = await updateTask(taskId, userId, req.body); + res.status(200).json({ message: 'Task updated successfully', task }); + } catch (error) { + next(error); + } +}; + +export const deleteTaskController = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const userId = (req as any).user.id; + const { taskId } = req.params; + + if (!taskId) { + res.status(400).json({ message: 'Task ID is required' }); + return; + } + + const result = await deleteTask(taskId, userId); + res.status(200).json(result); + } catch (error) { + next(error); + } +}; + +export const assignTaskController = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const userId = (req as any).user.id; + const { taskId } = req.params; + const { userIds } = req.body; + + if (!taskId) { + res.status(400).json({ message: 'Task ID is required' }); + return; + } + + if (!Array.isArray(userIds)) { + res.status(400).json({ message: 'userIds must be an array' }); + return; + } + + const result = await assignUsersToTask(taskId, userIds, userId); + res.status(200).json(result); + } catch (error) { + next(error); + } +}; + +export const getTaskStatisticsController = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const userId = (req as any).user.id; + const { teamId } = req.query; + + const statistics = await getTaskStatistics(userId, teamId as string); + res.status(200).json({ statistics }); + } catch (error) { + next(error); + } +}; \ No newline at end of file diff --git a/src/controllers/team.controller.ts b/src/controllers/team.controller.ts new file mode 100755 index 0000000..c87409b --- /dev/null +++ b/src/controllers/team.controller.ts @@ -0,0 +1,217 @@ +import { Request, Response, NextFunction } from 'express'; +import { + createTeam, + getTeams, + getTeamById, + updateTeam, + deleteTeam, + addTeamMember, + removeTeamMember, + updateTeamMemberRole, + leaveTeam +} from '../services/team.service'; +import { TeamRole } from '@prisma/client'; + +export const createTeamController = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + console.log('🔄 Team creation request received'); + console.log('👤 User ID:', (req as any).user.id); + console.log('📋 Request body:', req.body); + + const userId = (req as any).user.id; + console.log('📤 Calling team service...'); + const team = await createTeam(userId, req.body); + console.log('✅ Team created successfully:', team); + + res.status(201).json({ + success: true, + message: 'Team created successfully', + team + }); + } catch (error) { + console.error('❌ Error in team controller:', error); + next(error); + } +}; + +export const getTeamsController = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const userId = (req as any).user.id; + const teams = await getTeams(userId); + res.status(200).json({ teams }); + } catch (error) { + next(error); + } +}; + +export const getTeamByIdController = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const userId = (req as any).user.id; + const { teamId } = req.params; + + if (!teamId) { + res.status(400).json({ message: 'Team ID is required' }); + return; + } + + const team = await getTeamById(teamId, userId); + res.status(200).json({ team }); + } catch (error) { + next(error); + } +}; + +export const updateTeamController = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const userId = (req as any).user.id; + const { teamId } = req.params; + + if (!teamId) { + res.status(400).json({ message: 'Team ID is required' }); + return; + } + + const team = await updateTeam(teamId, userId, req.body); + res.status(200).json({ message: 'Team updated successfully', team }); + } catch (error) { + next(error); + } +}; + +export const deleteTeamController = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const userId = (req as any).user.id; + const { teamId } = req.params; + + if (!teamId) { + res.status(400).json({ message: 'Team ID is required' }); + return; + } + + const result = await deleteTeam(teamId, userId); + res.status(200).json(result); + } catch (error) { + next(error); + } +}; + +export const addTeamMemberController = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const userId = (req as any).user.id; + const { teamId } = req.params; + const { userId: memberUserId, role } = req.body; + + console.log('DEBUG - Add Team Member Request:'); + console.log('- teamId:', teamId); + console.log('- requester userId:', userId); + console.log('- request body:', req.body); + console.log('- memberUserId:', memberUserId); + console.log('- role:', role); + + if (!teamId) { + console.log('ERROR - Team ID is missing'); + res.status(400).json({ message: 'Team ID is required' }); + return; + } + + if (!memberUserId) { + console.log('ERROR - Member User ID is missing'); + res.status(400).json({ message: 'User ID is required' }); + return; + } + + try { + const membership = await addTeamMember(teamId, userId, { userId: memberUserId, role }); + console.log('SUCCESS - Member added:', membership); + res.status(201).json({ + success: true, + message: 'Member added successfully', + membership + }); + } catch (serviceError: any) { + console.log('Service Error:', serviceError.message); + if (serviceError.message.includes('already a member')) { + res.status(409).json({ + success: false, + message: 'User is already a member of this team' + }); + return; + } + if (serviceError.message.includes('not found')) { + res.status(404).json({ + success: false, + message: serviceError.message + }); + return; + } + if (serviceError.message.includes('insufficient permissions')) { + res.status(403).json({ + success: false, + message: 'You do not have permission to add members to this team' + }); + return; + } + throw serviceError; // Re-throw unexpected errors + } + } catch (error) { + console.log('ERROR in addTeamMemberController:', error); + next(error); + } +}; + +export const removeTeamMemberController = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const userId = (req as any).user.id; + const { teamId, memberId } = req.params; + + if (!teamId || !memberId) { + res.status(400).json({ message: 'Team ID and Member ID are required' }); + return; + } + + const result = await removeTeamMember(teamId, userId, memberId); + res.status(200).json(result); + } catch (error) { + next(error); + } +}; + +export const updateTeamMemberRoleController = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const userId = (req as any).user.id; + const { teamId, memberId } = req.params; + const { role } = req.body; + + if (!teamId || !memberId) { + res.status(400).json({ message: 'Team ID and Member ID are required' }); + return; + } + + if (!role || !Object.values(TeamRole).includes(role)) { + res.status(400).json({ message: 'Valid role is required' }); + return; + } + + const membership = await updateTeamMemberRole(teamId, userId, memberId, role); + res.status(200).json({ message: 'Role updated successfully', membership }); + } catch (error) { + next(error); + } +}; + +export const leaveTeamController = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const userId = (req as any).user.id; + const { teamId } = req.params; + + if (!teamId) { + res.status(400).json({ message: 'Team ID is required' }); + return; + } + + const result = await leaveTeam(teamId, userId); + res.status(200).json(result); + } catch (error) { + next(error); + } +}; \ No newline at end of file diff --git a/src/controllers/user.controller.js b/src/controllers/user.controller.js deleted file mode 100644 index 21d3ca8..0000000 --- a/src/controllers/user.controller.js +++ /dev/null @@ -1,60 +0,0 @@ -import * as userService from '../services/user.service.js'; - -export const createUser = async (req, res, next) => { - try { - const { email, firstName, lastName, password, imageUrl, location, address, phone, socialmedia } = req.body; - const user = await userService.register({ email, firstName, lastName, password, imageUrl, location, address, phone, socialmedia }); - res.status(201).json({ message: 'User registered', user }); - } catch (err) { - next(err); - } -}; - -export const checkEmailExists = async (req, res, next) => { - try { - const { email } = req.query; - if (!email) { - return res.status(400).json({ message: 'Email is required' }); - } - const exists = await userService.checkEmailExists(email); - res.status(200).json({ exists }); - } catch (err) { - next(err); - } -}; - -export const loginUser = async (req, res) => { - try { - const result = await userService.login(req.body); - res.status(200).json({ message: 'Login successful', ...result }); - } catch (err) { - res.status(401).json({ message: err.message }); - } -}; - -export const getUserProfile = async (req, res, next) => { - try { - const user = await userService.getProfile(req.user.id); - res.status(200).json(user); - } catch (err) { - next(err); - } -}; - -export const updateUserProfile = async (req, res, next) => { - try { - const updatedUser = await userService.updateProfile(req.user.id, req.body); - res.status(200).json({ message: 'Profile updated', user: updatedUser }); - } catch (err) { - next(err); - } -}; - -export const deleteUserProfile = async (req, res, next) => { - try { - await userService.deleteProfile(req.user.id); - res.status(200).json({ message: 'Profile deleted' }); - } catch (err) { - next(err); - } -}; \ No newline at end of file diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts new file mode 100755 index 0000000..bee3fa6 --- /dev/null +++ b/src/controllers/user.controller.ts @@ -0,0 +1,125 @@ +import type { Request, Response, NextFunction } from 'express'; +import { register, login, getProfile, updateProfile, deleteProfile, checkEmailExists, getUserById, searchUsersByEmail } from '../services/user.service'; + +export const createUser = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const { email, firstName, lastName, password, address, phone } = req.body; + const user = await register({ email, firstName, lastName, password,address, phone }); + res.status(201).json({ message: 'User registered', user }); + } catch (err) { + next(err); + } +}; + +export const checkEmailExistsController = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const { email } = req.query; + if (!email) { + res.status(400).json({ message: 'Email is required' }); + return; + } + const exists = await checkEmailExists(email as string); + res.status(200).json({ exists }); + } catch (err) { + next(err); + } +}; + +export const loginUser = async (req: Request, res: Response): Promise => { + try { + const result = await login(req.body); + res.status(200).json({ message: 'Login successful', ...result }); + } catch (err: any) { + res.status(401).json({ message: err.message }); + } +}; + +export const getUserProfile = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const user = await getProfile((req as any).user.id); + res.status(200).json({ user }); + } catch (err) { + next(err); + } +}; + +export const updateUserProfile = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const updateData = req.body; + const updatedUser = await updateProfile((req as any).user.id, updateData); + res.status(200).json({ message: 'Profile updated', user: updatedUser }); + } catch (err) { + next(err); + } +}; + +export const deleteUserProfile = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + await deleteProfile((req as any).user.id); + res.status(200).json({ message: 'Profile deleted' }); + } catch (err) { + next(err); + } +}; + +export const getUserByIdController = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + console.log('🔍 getUserByIdController called with params:', req.params); + console.log('🔍 getUserByIdController URL:', req.url); + console.log('🔍 getUserByIdController path:', req.path); + + const { userId } = req.params; + if (!userId) { + console.log('❌ getUserByIdController: No userId provided'); + res.status(400).json({ message: 'User ID is required' }); + return; + } + + console.log('📤 getUserByIdController: Looking for user with ID:', userId); + const user = await getUserById(userId); + res.status(200).json(user); + } catch (err) { + console.error('❌ getUserByIdController error:', err); + next(err); + } +}; + +export const searchUsersController = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + console.log('🔍 User search request received'); + console.log('📋 Query params:', req.query); + console.log('📋 Full URL:', req.url); + console.log('📋 Path:', req.path); + + const { email } = req.query; + if (!email || typeof email !== 'string') { + console.log('❌ Invalid email query parameter'); + res.status(400).json({ success: false, message: 'Email query parameter is required' }); + return; + } + + console.log('📤 Searching for users with email:', email); + const users = await searchUsersByEmail(email); + console.log('✅ Search completed, returning', users.length, 'users'); + + // Always return success, even if no users found + res.status(200).json({ success: true, users }); + } catch (err: any) { + console.error('❌ Error in searchUsersController:', err); + // Return empty array instead of error + res.status(200).json({ success: true, users: [] }); + } +}; + +// Test endpoint to verify routing is working +export const testSearchController = async (req: Request, res: Response): Promise => { + console.log('đŸ§Ē Test search endpoint hit!'); + res.status(200).json({ + success: true, + message: 'Search endpoint is working!', + query: req.query, + url: req.url, + path: req.path + }); +}; + diff --git a/src/index.ts b/src/index.ts new file mode 100755 index 0000000..c8040df --- /dev/null +++ b/src/index.ts @@ -0,0 +1,88 @@ +import express, { Request, Response, NextFunction } from 'express'; +import cors from 'cors'; +import dotenv from 'dotenv'; +import { createServer } from 'http'; +import SocketService from './utils/socket'; +import { setSocketService } from './services/activity.service'; +import { setTaskSocketService } from './services/task.service'; + +// Import routes +import userRoutes from './routes/user.routes'; +import taskRoutes from './routes/task.routes'; +import teamRoutes from './routes/team.routes'; +import commentRoutes from './routes/comment.routes'; +import activityRoutes from './routes/activity.routes'; +import dependencyRoutes from './routes/dependency.routes'; + +// Import middlewares +import { errorHandler, notFoundHandler } from './middlewares/error.middleware'; + +// Load environment variables +dotenv.config(); + +const app = express(); +const server = createServer(app); +const PORT = process.env.PORT || 3000; + +// Initialize Socket.IO +const socketService = new SocketService(server); +setSocketService(socketService); +setTaskSocketService(socketService); + +// Middleware +app.use(cors()); +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true })); + +// Types +interface ApiResponse { + message: string; + status: 'success' | 'error' | 'healthy'; + timestamp: string; + uptime?: number; +} + +// Health check routes +app.get('/', (req: Request, res: Response) => { + res.json({ + message: 'Task Management System Backend API', + status: 'success', + timestamp: new Date().toISOString() + }); +}); + +app.get('/api/health', (req: Request, res: Response) => { + res.json({ + message: 'Backend server is running!', + status: 'healthy', + uptime: process.uptime(), + timestamp: new Date().toISOString() + }); +}); + +// API Routes +app.use('/api/users', userRoutes); +app.use('/api/tasks', taskRoutes); +app.use('/api/teams', teamRoutes); +app.use('/api/comments', commentRoutes); +app.use('/api/activities', activityRoutes); +app.use('/api/dependencies', dependencyRoutes); + +// 404 handler +app.use(notFoundHandler); + +// Error handling middleware +app.use(errorHandler); + +// Start server +server.listen(PORT, () => { + console.log(`🚀 Server is running on port ${PORT}`); + console.log(`📊 Health check: http://localhost:${PORT}/api/health`); + console.log(`đŸ‘Ĩ User API: http://localhost:${PORT}/api/users`); + console.log(`📋 Task API: http://localhost:${PORT}/api/tasks`); + console.log(`đŸ‘Ĩ Team API: http://localhost:${PORT}/api/teams`); + console.log(`đŸ’Ŧ Comment API: http://localhost:${PORT}/api/comments`); + console.log(`📈 Activity API: http://localhost:${PORT}/api/activities`); + console.log(`🔗 Dependency API: http://localhost:${PORT}/api/dependencies`); + console.log(`⚡ WebSocket server is running`); +}); \ No newline at end of file diff --git a/src/middlewares/auth.middleware.js b/src/middlewares/auth.middleware.js deleted file mode 100644 index b2e7d66..0000000 --- a/src/middlewares/auth.middleware.js +++ /dev/null @@ -1,20 +0,0 @@ -import jwt from 'jsonwebtoken'; -const { verify } = jwt; - -export default (req, res, next) => { - const authHeader = req.headers['authorization']; - - if (!authHeader || !authHeader.startsWith('Bearer ')) { - return res.status(401).json({ message: 'Unauthorized: Token missing' }); - } - - const token = authHeader.split(' ')[1]; - - try { - const decoded = verify(token, process.env.JWT_SECRET); - req.user = decoded; // Add user info to request - next(); - } catch (err) { - return res.status(401).json({ message: 'Unauthorized: Token invalid' }); - } -}; \ No newline at end of file diff --git a/src/middlewares/auth.middleware.ts b/src/middlewares/auth.middleware.ts new file mode 100755 index 0000000..e6653af --- /dev/null +++ b/src/middlewares/auth.middleware.ts @@ -0,0 +1,38 @@ +import type { Request, Response, NextFunction } from 'express'; +import jwt from 'jsonwebtoken'; + +const JWT_SECRET = process.env.JWT_SECRET; + +if (!JWT_SECRET) { + throw new Error('JWT_SECRET environment variable is required'); +} + +export default (req: Request, res: Response, next: NextFunction): void => { + console.log('🔐 Auth middleware - Path:', req.path, 'Method:', req.method); + const authHeader = req.headers['authorization']; + + if (!authHeader || !authHeader.startsWith('Bearer ')) { + console.log('❌ No valid authorization header'); + res.status(401).json({ message: 'Unauthorized: Token missing' }); + return; + } + + const token = authHeader.split(' ')[1]; + + if (!token) { + console.log('❌ Token malformed'); + res.status(401).json({ message: 'Unauthorized: Token malformed' }); + return; + } + + try { + const decoded = jwt.verify(token, JWT_SECRET); + console.log('✅ Token verified for user:', (decoded as any).userId); + (req as any).user = decoded; // Add user info to request + next(); + } catch (err) { + console.log('❌ Token invalid:', err); + res.status(401).json({ message: 'Unauthorized: Token invalid' }); + return; + } +}; \ No newline at end of file diff --git a/src/middlewares/database.middleware.ts b/src/middlewares/database.middleware.ts new file mode 100755 index 0000000..97a588d --- /dev/null +++ b/src/middlewares/database.middleware.ts @@ -0,0 +1,52 @@ +import { Request, Response, NextFunction } from 'express'; +import { PrismaClientKnownRequestError, PrismaClientUnknownRequestError } from '@prisma/client/runtime/library'; + +export const databaseErrorHandler = (error: any, req: Request, res: Response, next: NextFunction) => { + console.error('Database Error:', error); + + // Handle Prisma connection errors + if (error.code === 'P1001') { + return res.status(503).json({ + error: 'Database connection failed', + message: 'Unable to connect to the database. Please try again later.', + code: 'DATABASE_CONNECTION_ERROR' + }); + } + + // Handle other Prisma errors + if (error instanceof PrismaClientKnownRequestError) { + switch (error.code) { + case 'P2002': + return res.status(409).json({ + error: 'Unique constraint violation', + message: 'A record with this data already exists.', + code: 'DUPLICATE_RECORD' + }); + + case 'P2025': + return res.status(404).json({ + error: 'Record not found', + message: 'The requested record does not exist.', + code: 'RECORD_NOT_FOUND' + }); + + default: + return res.status(500).json({ + error: 'Database operation failed', + message: 'An error occurred while processing your request.', + code: 'DATABASE_ERROR' + }); + } + } + + if (error instanceof PrismaClientUnknownRequestError) { + return res.status(500).json({ + error: 'Unknown database error', + message: 'An unexpected database error occurred.', + code: 'UNKNOWN_DATABASE_ERROR' + }); + } + + // Pass other errors to the next error handler + next(error); +}; diff --git a/src/middlewares/error.middleware.ts b/src/middlewares/error.middleware.ts new file mode 100755 index 0000000..237ffc1 --- /dev/null +++ b/src/middlewares/error.middleware.ts @@ -0,0 +1,78 @@ +import { Request, Response, NextFunction } from 'express'; + +// Error interface +interface CustomError extends Error { + statusCode?: number; + status?: string; +} + +// Global error handler middleware +export const errorHandler = ( + err: CustomError, + req: Request, + res: Response, + next: NextFunction +): void => { + // Set default error values + let statusCode = err.statusCode || 500; + let message = err.message || 'Internal Server Error'; + + // Handle specific error types + if (err.name === 'ValidationError') { + statusCode = 400; + message = 'Validation Error'; + } + + if (err.name === 'CastError') { + statusCode = 400; + message = 'Invalid data format'; + } + + if (err.name === 'JsonWebTokenError') { + statusCode = 401; + message = 'Invalid token'; + } + + if (err.name === 'TokenExpiredError') { + statusCode = 401; + message = 'Token expired'; + } + + // Prisma specific errors + if (err.name === 'PrismaClientKnownRequestError') { + statusCode = 400; + message = 'Database operation failed'; + } + + // Log error in development + if (process.env.NODE_ENV === 'development') { + console.error('Error Stack:', err.stack); + } + + // Send error response + res.status(statusCode).json({ + success: false, + message, + ...(process.env.NODE_ENV === 'development' && { stack: err.stack }) + }); +}; + +// 404 Not Found handler +export const notFoundHandler = ( + req: Request, + res: Response, + next: NextFunction +): void => { + const message = `Route ${req.originalUrl} not found`; + res.status(404).json({ + success: false, + message + }); +}; + +// Async error handler wrapper +export const asyncHandler = (fn: Function) => { + return (req: Request, res: Response, next: NextFunction) => { + Promise.resolve(fn(req, res, next)).catch(next); + }; +}; \ No newline at end of file diff --git a/src/middlewares/validation.middleware.js b/src/middlewares/validation.middleware.ts old mode 100644 new mode 100755 similarity index 84% rename from src/middlewares/validation.middleware.js rename to src/middlewares/validation.middleware.ts index 7929a94..a9fca0b --- a/src/middlewares/validation.middleware.js +++ b/src/middlewares/validation.middleware.ts @@ -1,9 +1,11 @@ +import type { Request, Response, NextFunction } from 'express'; + /** * Validation middleware that can validate different parts of the request * @param {Object} schema - Joi validation schema * @param {string} source - Source to validate: 'body', 'query', 'params' (default: 'body') */ -export default (schema, source = 'body') => (req, res, next) => { +export default (schema: any, source = 'body') => (req: Request, res: Response, next: NextFunction): void => { console.log(`=== Validation Middleware (${source}) ===`); let dataToValidate; @@ -32,14 +34,15 @@ export default (schema, source = 'body') => (req, res, next) => { console.log('Validation Error:', error.details[0].message); console.log('Full error details:', error.details); - return res.status(400).json({ + res.status(400).json({ success: false, - message: 'Validation failed', - errors: error.details.map(detail => ({ + message: error.details[0].message, + errors: error.details.map((detail: any) => ({ field: detail.path.join('.'), message: detail.message })) }); + return; } // Replace the validated data with the sanitized version diff --git a/src/routes/activity.routes.ts b/src/routes/activity.routes.ts new file mode 100755 index 0000000..e9bd90c --- /dev/null +++ b/src/routes/activity.routes.ts @@ -0,0 +1,14 @@ +import { Router } from 'express'; +import { getActivitiesController } from '../controllers/activity.controller'; +import { activityQuerySchema } from '../validators/activity.validator'; +import validate from '../middlewares/validation.middleware'; +import authMiddleware from '../middlewares/auth.middleware'; + +const router = Router(); + +// All activity routes require authentication +router.use(authMiddleware); + +router.get('/', validate(activityQuerySchema, 'query'), getActivitiesController); + +export default router; \ No newline at end of file diff --git a/src/routes/catagory.route.js b/src/routes/catagory.route.js deleted file mode 100644 index e9c554e..0000000 --- a/src/routes/catagory.route.js +++ /dev/null @@ -1,121 +0,0 @@ -import { Router } from 'express'; -const router = Router(); - -import { - createCategory, - getCategories, - getCategoryById, - getCategoryBySlug, - updateCategory, - deleteCategory, - getRootCategories, - getCategoryHierarchy, - searchCategories -} from '../controllers/catagory.controller.js'; - -import validate from '../middlewares/validation.middleware.js'; -import { - createCategorySchema, - updateCategorySchema, - categoryIdSchema, - categorySlugSchema, - searchCategoriesSchema, - categoryQuerySchema -} from '../validators/catagory.validator.js'; - -import authMiddleware from '../middlewares/auth.middleware.js'; - -// Public routes (no authentication required) -/** - * @route GET /api/categories - * @desc Get all categories with optional filtering - * @access Public - * @query parentId - Filter by parent category ID - * @query includeChildren - Include children categories (default: true) - * @query includeParent - Include parent category info (default: true) - * @query includeServices - Include services count (default: false) - */ -router.get('/', validate(categoryQuerySchema, 'query'), getCategories); - -/** - * @route GET /api/categories/roots - * @desc Get all root categories (categories with no parent) - * @access Public - * @query includeChildren - Include children categories (default: true) - */ -router.get('/roots', validate(categoryQuerySchema, 'query'), getRootCategories); - -/** - * @route GET /api/categories/search - * @desc Search categories by name or description - * @access Public - * @query q - Search term (required) - * @query includeChildren - Include children categories (default: true) - * @query includeParent - Include parent category info (default: true) - */ -router.get('/search', validate(searchCategoriesSchema, 'query'), searchCategories); - -/** - * @route GET /api/categories/id/:id - * @desc Get category by ID - * @access Public - * @param id - Category ID - * @query includeChildren - Include children categories (default: true) - * @query includeParent - Include parent category info (default: true) - * @query includeServices - Include services (default: false) - */ -router.get('/id/:id', validate(categoryIdSchema, 'params'), getCategoryById); - -/** - * @route GET /api/categories/slug/:slug - * @desc Get category by slug - * @access Public - * @param slug - Category slug - * @query includeChildren - Include children categories (default: true) - * @query includeParent - Include parent category info (default: true) - * @query includeServices - Include services (default: false) - */ -router.get('/slug/:slug', validate(categorySlugSchema, 'params'), getCategoryBySlug); - -/** - * @route GET /api/categories/:id/hierarchy - * @desc Get category hierarchy (full tree starting from category) - * @access Public - * @param id - Category ID - */ -router.get('/:id/hierarchy', validate(categoryIdSchema, 'params'), getCategoryHierarchy); - -// Protected routes (authentication required) -/** - * @route POST /api/categories - * @desc Create a new category - * @access Public (For Testing) - * @body name - Category name (optional) - * @body slug - Category slug (required, unique) - * @body description - Category description (optional) - * @body parentId - Parent category ID (optional) - */ -router.post('/', validate(createCategorySchema), createCategory); - -/** - * @route PUT /api/categories/:id - * @desc Update a category - * @access Private (Admin/Provider) - * @param id - Category ID - * @body name - Category name (optional) - * @body slug - Category slug (optional) - * @body description - Category description (optional) - * @body parentId - Parent category ID (optional) - */ -router.put('/:id', authMiddleware, validate(categoryIdSchema, 'params'), validate(updateCategorySchema), updateCategory); - -/** - * @route DELETE /api/categories/:id - * @desc Delete a category - * @access Private (Admin only) - * @param id - Category ID - * @query force - Force delete even if category has children or services (default: false) - */ -router.delete('/:id', authMiddleware, validate(categoryIdSchema, 'params'), deleteCategory); - -export default router; diff --git a/src/routes/comment.routes.ts b/src/routes/comment.routes.ts new file mode 100755 index 0000000..60ae13f --- /dev/null +++ b/src/routes/comment.routes.ts @@ -0,0 +1,27 @@ +import { Router } from 'express'; +import { + createCommentController, + getTaskCommentsController, + updateCommentController, + deleteCommentController +} from '../controllers/comment.controller'; +import { + createCommentSchema, + updateCommentSchema, + commentQuerySchema +} from '../validators/comment.validator'; +import validate from '../middlewares/validation.middleware'; +import authMiddleware from '../middlewares/auth.middleware'; + +const router = Router(); + +// All comment routes require authentication +router.use(authMiddleware); + +// Comment CRUD operations +router.post('/', validate(createCommentSchema), createCommentController); +router.get('/task/:taskId', validate(commentQuerySchema, 'query'), getTaskCommentsController); +router.put('/:commentId', validate(updateCommentSchema), updateCommentController); +router.delete('/:commentId', deleteCommentController); + +export default router; \ No newline at end of file diff --git a/src/routes/company.route.js b/src/routes/company.route.js deleted file mode 100644 index d25d86a..0000000 --- a/src/routes/company.route.js +++ /dev/null @@ -1,24 +0,0 @@ -import express from 'express'; -import * as companyController from '../controllers/company.controller.js'; -import authMiddleware from '../middlewares/auth.middleware.js'; -import validationMiddleware from '../middlewares/validation.middleware.js'; -import { createCompanySchema, updateCompanySchema } from '../validators/company.validator.js'; - -const router = express.Router(); - -// All routes require authentication -router.use(authMiddleware); - -// Get all companies for the authenticated provider -router.get('/', companyController.getCompanies); - -// Create a new company -router.post('/', validationMiddleware(createCompanySchema), companyController.createCompany); - -// Update a company -router.put('/:companyId', validationMiddleware(updateCompanySchema), companyController.updateCompany); - -// Delete a company -router.delete('/:companyId', companyController.deleteCompany); - -export default router; diff --git a/src/routes/dependency.routes.ts b/src/routes/dependency.routes.ts new file mode 100755 index 0000000..46b56c8 --- /dev/null +++ b/src/routes/dependency.routes.ts @@ -0,0 +1,28 @@ +import { Router } from 'express'; +import { + addTaskDependencyController, + removeTaskDependencyController, + getTaskDependenciesController, + validateTaskCompletionController, + getDependencyGraphController +} from '../controllers/dependency.controller'; +import { + createDependencySchema, + dependencyQuerySchema +} from '../validators/dependency.validator'; +import validate from '../middlewares/validation.middleware'; +import authMiddleware from '../middlewares/auth.middleware'; + +const router = Router(); + +// All dependency routes require authentication +router.use(authMiddleware); + +// Dependency management +router.post('/', validate(createDependencySchema), addTaskDependencyController); +router.delete('/:dependencyId', removeTaskDependencyController); +router.get('/task/:taskId', getTaskDependenciesController); +router.get('/task/:taskId/validate-completion', validateTaskCompletionController); +router.get('/graph', validate(dependencyQuerySchema, 'query'), getDependencyGraphController); + +export default router; \ No newline at end of file diff --git a/src/routes/provider.route.js b/src/routes/provider.route.js deleted file mode 100644 index c27f414..0000000 --- a/src/routes/provider.route.js +++ /dev/null @@ -1,18 +0,0 @@ -import { Router } from 'express'; -const router = Router(); -import { - createProvider, - updateProvider, - deleteProvider, - getProviderProfile -} from '../controllers/provider.controller.js'; -import validate from '../middlewares/validation.middleware.js'; -import { createProviderSchema, updateProviderSchema } from '../validators/provider.validator.js'; -import authMiddleware from '../middlewares/auth.middleware.js'; - -router.post('/', authMiddleware, validate(createProviderSchema), createProvider); -router.get('/profile', authMiddleware, getProviderProfile); -router.put('/profile', authMiddleware, validate(updateProviderSchema), updateProvider); -router.delete('/profile', authMiddleware, deleteProvider); - -export default router; diff --git a/src/routes/provider.route.ts b/src/routes/provider.route.ts new file mode 100755 index 0000000..2603f52 --- /dev/null +++ b/src/routes/provider.route.ts @@ -0,0 +1,27 @@ +import { Router } from 'express'; +const router: import('express').Router = Router(); +import { + createProvider, + updateProvider, + deleteProvider, + getProviderProfile, + getProviderById, + verifyProvider, + unverifyProvider +} from '../controllers/provider.controller.js'; +import validate from '../middlewares/validation.middleware.js'; +import { createProviderSchema, updateProviderSchema, providerParamsSchema } from '../validators/provider.validator.js'; +import authMiddleware from '../middlewares/auth.middleware.js'; +import { adminAuthMiddleware } from '../Admin/middlewares/admin.middleware.js'; + +router.post('/', authMiddleware, validate(createProviderSchema), createProvider); +router.get('/profile', authMiddleware, getProviderProfile); +router.put('/profile', authMiddleware, validate(updateProviderSchema), updateProvider); +router.delete('/profile', authMiddleware, deleteProvider); +router.get('/:id', validate(providerParamsSchema, 'params'), getProviderById); + +// Verification routes (admin only) +router.put('/:id/verify', authMiddleware, adminAuthMiddleware, validate(providerParamsSchema, 'params'), verifyProvider); +router.put('/:id/unverify', authMiddleware, adminAuthMiddleware, validate(providerParamsSchema, 'params'), unverifyProvider); + +export default router; diff --git a/src/routes/services.route.js b/src/routes/services.route.js deleted file mode 100644 index 2fddd54..0000000 --- a/src/routes/services.route.js +++ /dev/null @@ -1,62 +0,0 @@ -import { Router } from 'express'; -import { - createService, - getServices, - getServiceById, - updateService, - deleteService -} from '../controllers/services.controller.js'; -import validate from '../middlewares/validation.middleware.js'; -import { - createServiceSchema, - updateServiceSchema, - getServicesQuerySchema, - serviceIdSchema -} from '../validators/services.validator.js'; -import authMiddleware from '../middlewares/auth.middleware.js'; - -const router = Router(); - -// Apply authentication middleware to all routes -// router.use(authMiddleware); - -/** - * @route POST /api/services - * @desc Create a new service - * @access Private (Service Provider only) - */ -router.post('/', validate(createServiceSchema), createService); - -/** - * @route GET /api/services - * @desc Get all services with optional filtering - * @access Public - */ -router.get('/', validate(getServicesQuerySchema, 'query'), getServices); - -/** - * @route GET /api/services/:id - * @desc Get a single service by ID - * @access Public - */ -router.get('/:id', validate(serviceIdSchema, 'params'), getServiceById); - -/** - * @route PUT /api/services/:id - * @desc Update a service - * @access Private (Service Provider - own services only) - */ -router.put('/:id', - validate(serviceIdSchema, 'params'), - validate(updateServiceSchema), - updateService -); - -/** - * @route DELETE /api/services/:id - * @desc Delete a service - * @access Private (Service Provider - own services only) - */ -router.delete('/:id', validate(serviceIdSchema, 'params'), deleteService); - -export default router; diff --git a/src/routes/services.route.ts b/src/routes/services.route.ts new file mode 100755 index 0000000..a797afc --- /dev/null +++ b/src/routes/services.route.ts @@ -0,0 +1,146 @@ +import { Router } from 'express'; +import { + createService, + getServices, + getServiceById, + updateService, + deleteService, + getServiceByConversationId, + searchServices, + hybridSearchServices, + getSimilarServices, + updateServiceEmbeddings, + updateAllServiceEmbeddings, + searchServicesByLocation, + getLocationFromIP, + geocodeAddress, + reverseGeocode +} from '../controllers/services.controller.js'; +import validate from '../middlewares/validation.middleware.js'; +import { + createServiceSchema, + updateServiceSchema, + getServicesQuerySchema, + serviceIdSchema, + conversationIdSchema, + searchServicesByLocationSchema, + geocodeAddressSchema, + reverseGeocodeSchema +} from '../validators/services.validator.js'; +import authMiddleware from '../middlewares/auth.middleware.js'; + +const router: import('express').Router = Router(); + +// Apply authentication middleware to all routes +// router.use(authMiddleware); + +/** + * @route POST /api/services + * @desc Create a new service + * @access Private (Service Provider only) + */ +router.post('/', validate(createServiceSchema), createService); + +/** + * @route GET /api/services/search/hybrid + * @desc Hybrid search for services (semantic + location) + * @access Public + */ +router.get('/search/hybrid', hybridSearchServices); + +/** + * @route GET /api/services/search + * @desc Semantic search for services + * @access Public + */ +router.get('/search', searchServices); + +/** + * @route GET /api/services/search/location + * @desc Search services by location with radius filtering + * @access Public + */ +router.get('/search/location', validate(searchServicesByLocationSchema, 'query'), searchServicesByLocation); + +/** + * @route GET /api/services/location/ip + * @desc Get location information from IP address + * @access Public + */ +router.get('/location/ip', getLocationFromIP); + +/** + * @route POST /api/services/location/geocode + * @desc Convert address to coordinates + * @access Public + */ +router.post('/location/geocode', validate(geocodeAddressSchema), geocodeAddress); + +/** + * @route POST /api/services/location/reverse-geocode + * @desc Convert coordinates to address + * @access Public + */ +router.post('/location/reverse-geocode', validate(reverseGeocodeSchema), reverseGeocode); + +/** + * @route POST /api/services/embeddings/batch + * @desc Batch update embeddings for all services + * @access Private (Admin) + */ +router.post('/embeddings/batch', updateAllServiceEmbeddings); + +/** + * @route GET /api/services + * @desc Get all services with optional filtering + * @access Public + */ +router.get('/', validate(getServicesQuerySchema, 'query'), getServices); + +/** + * @route GET /api/services/:id + * @desc Get a single service by ID + * @access Public + */ +router.get('/:id', validate(serviceIdSchema, 'params'), getServiceById); + +/** + * @route GET /api/services/conversation/:conversationId + * @desc Get a service by conversation ID + * @access Public + */ +router.get('/conversation/:conversationId', validate(conversationIdSchema, 'params'), getServiceByConversationId); + +/** + * @route GET /api/services/:id/similar + * @desc Get similar services to a given service + * @access Public + */ +router.get('/:id/similar', validate(serviceIdSchema, 'params'), getSimilarServices); + +/** + * @route POST /api/services/:id/embeddings + * @desc Update embeddings for a specific service + * @access Private (Admin) + */ +router.post('/:id/embeddings', validate(serviceIdSchema, 'params'), updateServiceEmbeddings); + +/** + * @route PUT /api/services/:id + * @desc Update a service + * @access Private (Service Provider - own services only) + */ +router.put('/:id', + validate(serviceIdSchema, 'params'), + validate(updateServiceSchema), + updateService +); + +/** + * @route DELETE /api/services/:id + * @desc Delete a service + * @access Private (Service Provider - own services only) + */ +router.delete('/:id', validate(serviceIdSchema, 'params'), deleteService); + +export default router; diff --git a/src/routes/task.routes.ts b/src/routes/task.routes.ts new file mode 100755 index 0000000..7a17a25 --- /dev/null +++ b/src/routes/task.routes.ts @@ -0,0 +1,36 @@ +import { Router } from 'express'; +import { + createTaskController, + getTasksController, + getTaskByIdController, + updateTaskController, + deleteTaskController, + assignTaskController, + getTaskStatisticsController +} from '../controllers/task.controller'; +import { + createTaskSchema, + updateTaskSchema, + assignTaskSchema, + taskQuerySchema +} from '../validators/task.validator'; +import validate from '../middlewares/validation.middleware'; +import authMiddleware from '../middlewares/auth.middleware'; + +const router = Router(); + +// All task routes require authentication +router.use(authMiddleware); + +// Task CRUD operations +router.post('/', validate(createTaskSchema), createTaskController); +router.get('/', validate(taskQuerySchema, 'query'), getTasksController); +router.get('/statistics', getTaskStatisticsController); +router.get('/:taskId', getTaskByIdController); +router.put('/:taskId', validate(updateTaskSchema), updateTaskController); +router.delete('/:taskId', deleteTaskController); + +// Task assignment +router.post('/:taskId/assign', validate(assignTaskSchema), assignTaskController); + +export default router; \ No newline at end of file diff --git a/src/routes/team.routes.ts b/src/routes/team.routes.ts new file mode 100755 index 0000000..129f984 --- /dev/null +++ b/src/routes/team.routes.ts @@ -0,0 +1,40 @@ +import { Router } from 'express'; +import { + createTeamController, + getTeamsController, + getTeamByIdController, + updateTeamController, + deleteTeamController, + addTeamMemberController, + removeTeamMemberController, + updateTeamMemberRoleController, + leaveTeamController +} from '../controllers/team.controller'; +import { + createTeamSchema, + updateTeamSchema, + addTeamMemberSchema, + updateTeamMemberRoleSchema +} from '../validators/team.validator'; +import validate from '../middlewares/validation.middleware'; +import authMiddleware from '../middlewares/auth.middleware'; + +const router = Router(); + +// All team routes require authentication +router.use(authMiddleware); + +// Team CRUD operations +router.post('/', validate(createTeamSchema), createTeamController); +router.get('/', getTeamsController); +router.get('/:teamId', getTeamByIdController); +router.put('/:teamId', validate(updateTeamSchema), updateTeamController); +router.delete('/:teamId', deleteTeamController); + +// Team membership management +router.post('/:teamId/members', validate(addTeamMemberSchema), addTeamMemberController); +router.delete('/:teamId/members/:memberId', removeTeamMemberController); +router.put('/:teamId/members/:memberId/role', validate(updateTeamMemberRoleSchema), updateTeamMemberRoleController); +router.post('/:teamId/leave', leaveTeamController); + +export default router; \ No newline at end of file diff --git a/src/routes/user.route.js b/src/routes/user.route.js deleted file mode 100644 index d1ad645..0000000 --- a/src/routes/user.route.js +++ /dev/null @@ -1,15 +0,0 @@ -import { Router } from 'express'; -const router = Router(); -import { createUser,loginUser,getUserProfile,updateUserProfile,deleteUserProfile,checkEmailExists} from '../controllers/user.controller.js'; -import validate from '../middlewares/validation.middleware.js'; -import { registerSchema,loginSchema,updateProfileSchema } from '../validators/user.validator.js'; -import authMiddleware from '../middlewares/auth.middleware.js'; - -router.get('/check-email', checkEmailExists); -router.post('/register', validate(registerSchema), createUser); -router.post('/login', validate(loginSchema), loginUser); -router.get('/profile', authMiddleware, getUserProfile); -router.put('/profile', authMiddleware, validate(updateProfileSchema), updateUserProfile); -router.delete('/profile', authMiddleware, deleteUserProfile); - -export default router; diff --git a/src/routes/user.route.ts b/src/routes/user.route.ts new file mode 100755 index 0000000..bc0d77c --- /dev/null +++ b/src/routes/user.route.ts @@ -0,0 +1,24 @@ +import { Router } from 'express'; +const router: import('express').Router = Router(); +import { createUser,loginUser,getUserProfile,updateUserProfile,deleteUserProfile,checkEmailExistsController,searchUsersController,uploadImageController,createAdminUser,getUserByIdController,uploadVideoController } from '../controllers/user.controller.js'; +import validate from '../middlewares/validation.middleware.js'; +import { registerSchema,loginSchema,updateProfileSchema } from '../validators/user.validator.js'; +import authMiddleware from '../middlewares/auth.middleware.js'; +import { adminAuthMiddleware } from '../Admin/middlewares/admin.middleware.js'; +import { upload, uploadVideo } from '../utils/s3.js'; + +router.get('/check-email', checkEmailExistsController); +router.get('/search', authMiddleware, searchUsersController); +router.get('/profile', authMiddleware, getUserProfile); +router.get('/:userId', getUserByIdController); +router.post('/register', validate(registerSchema), createUser); +router.post('/login', validate(loginSchema), loginUser); +router.put('/profile', authMiddleware, upload.single('profileImage'), updateUserProfile); +router.delete('/profile', authMiddleware, deleteUserProfile); +router.post('/upload-image', authMiddleware, upload.single('image'), uploadImageController); +router.post('/upload-video', authMiddleware, uploadVideo.single('video'), uploadVideoController); + +// Admin creation route (admin only) +router.post('/admin', authMiddleware, adminAuthMiddleware, validate(registerSchema), createAdminUser); + +export default router; diff --git a/src/routes/user.routes.ts b/src/routes/user.routes.ts new file mode 100755 index 0000000..3642b7a --- /dev/null +++ b/src/routes/user.routes.ts @@ -0,0 +1,20 @@ +import { Router } from 'express'; +const router: import('express').Router = Router(); +import { createUser, loginUser, getUserProfile, updateUserProfile, deleteUserProfile, checkEmailExistsController, getUserByIdController, searchUsersController, testSearchController } from '../controllers/user.controller'; +import { registerSchema, loginSchema } from '../validators/user.validator'; +import validate from '../middlewares/validation.middleware'; +import authMiddleware from '../middlewares/auth.middleware'; + +router.get('/check-email', checkEmailExistsController); +router.get('/test-search', testSearchController); // Test endpoint (no auth needed) +router.get('/find-users', authMiddleware, searchUsersController); // Alternative endpoint +router.get('/search', authMiddleware, searchUsersController); +router.get('/profile', authMiddleware, getUserProfile); +router.get('/:userId', getUserByIdController); +router.post('/register', validate(registerSchema), createUser); +router.post('/login', validate(loginSchema), loginUser); +router.put('/profile', authMiddleware, updateUserProfile); +router.delete('/profile', authMiddleware, deleteUserProfile); + + +export default router; diff --git a/src/services/activity.service.ts b/src/services/activity.service.ts new file mode 100755 index 0000000..6c1f9a2 --- /dev/null +++ b/src/services/activity.service.ts @@ -0,0 +1,120 @@ +import { prisma } from '../utils/database'; +import { ActivityType, EntityType } from '@prisma/client'; + +export interface CreateActivityData { + type: ActivityType; + description: string; + entityType: EntityType; + entityId: string; + userId?: string; + teamId?: string; + metadata?: Record; +} + +// Global socket service instance (will be set from the main app) +let socketService: any = null; + +export const setSocketService = (service: any) => { + socketService = service; +}; + +export const createActivity = async (activityData: CreateActivityData) => { + const activity = await prisma.activity.create({ + data: activityData, + include: { + user: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + }, + team: { + select: { + id: true, + name: true + } + } + } + }); + + // Broadcast activity in real-time + if (socketService) { + socketService.broadcastActivity(activity); + } + + return activity; +}; + +export const getActivities = async ( + userId: string, + filters: { + teamId?: string; + entityType?: EntityType; + limit?: number; + } = {} +) => { + const { teamId, entityType, limit = 50 } = filters; + + const where: any = {}; + + if (teamId) { + where.teamId = teamId; + } else { + // Get activities for tasks/teams user is involved in + where.OR = [ + { userId }, + { teamId: { in: await getUserTeamIds(userId) } }, + { + AND: [ + { entityType: EntityType.TASK }, + { + task: { + OR: [ + { createdById: userId }, + { assignments: { some: { userId } } }, + { team: { members: { some: { userId } } } } + ] + } + } + ] + } + ]; + } + + if (entityType) { + where.entityType = entityType; + } + + return await prisma.activity.findMany({ + where, + take: limit, + orderBy: { createdAt: 'desc' }, + include: { + user: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + }, + team: { + select: { + id: true, + name: true + } + } + } + }); +}; + +const getUserTeamIds = async (userId: string): Promise => { + const memberships = await prisma.teamMember.findMany({ + where: { userId }, + select: { teamId: true } + }); + + return memberships.map(m => m.teamId); +}; \ No newline at end of file diff --git a/src/services/catagory.service.js b/src/services/catagory.service.js deleted file mode 100644 index 6fe1535..0000000 --- a/src/services/catagory.service.js +++ /dev/null @@ -1,547 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -/** - * Create a new category - * @param {Object} categoryData - The category data - * @param {string} [categoryData.name] - Category name - * @param {string} categoryData.slug - Unique slug for the category - * @param {string} [categoryData.description] - Category description - * @param {string} [categoryData.parentId] - Parent category ID for hierarchical structure - * @returns {Promise} Created category object - */ -export const createCategory = async (categoryData) => { - try { - const { name, slug, description, parentId } = categoryData; - - // Validate required fields - if (!slug) { - throw new Error('Slug is required'); - } - - // Check if slug already exists - const existingCategory = await prisma.category.findUnique({ - where: { slug } - }); - if (existingCategory) { - const err = new Error('Category with this slug already exists'); - err.name = 'BadRequestError'; - err.status = 400; - throw err; - } - - // If parentId is provided, validate that parent exists - if (parentId) { - const parentCategory = await prisma.category.findUnique({ - where: { id: parentId } - }); - if (!parentCategory) { - throw new Error('Parent category not found'); - } - } - - // Create the category - const newCategory = await prisma.category.create({ - data: { - name, - slug, - description, - parentId - }, - include: { - parent: { - select: { - id: true, - name: true, - slug: true - } - }, - children: { - select: { - id: true, - name: true, - slug: true - } - }, - _count: { - select: { - services: true - } - } - } - }); - - return newCategory; - } catch (error) { - throw new Error(`Failed to create category: ${error.message}`); - } -}; - -/** - * Get all categories with optional filtering - * @param {Object} filters - Optional filters - * @param {string} [filters.parentId] - Filter by parent category ID - * @param {boolean} [filters.includeChildren=true] - Include children categories - * @param {boolean} [filters.includeParent=true] - Include parent category info - * @param {boolean} [filters.includeServices=false] - Include services count - * @returns {Promise} Array of category objects - */ -export const getAllCategories = async (filters = {}) => { - try { - const { - parentId, - includeChildren = true, - includeParent = true, - includeServices = false - } = filters; - - const whereClause = {}; - if (parentId !== undefined) { - whereClause.parentId = parentId; - } - - const categories = await prisma.category.findMany({ - where: whereClause, - include: { - parent: includeParent ? { - select: { - id: true, - name: true, - slug: true - } - } : false, - children: includeChildren ? { - select: { - id: true, - name: true, - slug: true, - description: true - } - } : false, - _count: includeServices ? { - select: { - services: true - } - } : false - }, - orderBy: { - name: 'asc' - } - }); - - return categories; - } catch (error) { - throw new Error(`Failed to fetch categories: ${error.message}`); - } -}; - -/** - * Get category by ID - * @param {string} id - Category ID - * @param {Object} options - Additional options - * @param {boolean} [options.includeChildren=true] - Include children categories - * @param {boolean} [options.includeParent=true] - Include parent category info - * @param {boolean} [options.includeServices=false] - Include services - * @returns {Promise} Category object or null if not found - */ -export const getCategoryById = async (id, options = {}) => { - try { - const { - includeChildren = true, - includeParent = true, - includeServices = false - } = options; - - const category = await prisma.category.findUnique({ - where: { id }, - include: { - parent: includeParent ? { - select: { - id: true, - name: true, - slug: true, - description: true - } - } : false, - children: includeChildren ? { - select: { - id: true, - name: true, - slug: true, - description: true - } - } : false, - services: includeServices ? { - select: { - id: true, - title: true, - description: true, - price: true, - currency: true, - isActive: true - } - } : false, - _count: { - select: { - services: true - } - } - } - }); - - return category; - } catch (error) { - throw new Error(`Failed to fetch category: ${error.message}`); - } -}; - -/** - * Get category by slug - * @param {string} slug - Category slug - * @param {Object} options - Additional options - * @param {boolean} [options.includeChildren=true] - Include children categories - * @param {boolean} [options.includeParent=true] - Include parent category info - * @param {boolean} [options.includeServices=false] - Include services - * @returns {Promise} Category object or null if not found - */ -export const getCategoryBySlug = async (slug, options = {}) => { - try { - const { - includeChildren = true, - includeParent = true, - includeServices = false - } = options; - - const category = await prisma.category.findUnique({ - where: { slug }, - include: { - parent: includeParent ? { - select: { - id: true, - name: true, - slug: true, - description: true - } - } : false, - children: includeChildren ? { - select: { - id: true, - name: true, - slug: true, - description: true - } - } : false, - services: includeServices ? { - select: { - id: true, - title: true, - description: true, - price: true, - currency: true, - isActive: true - } - } : false, - _count: { - select: { - services: true - } - } - } - }); - - return category; - } catch (error) { - throw new Error(`Failed to fetch category: ${error.message}`); - } -}; - -/** - * Update a category - * @param {string} id - Category ID - * @param {Object} updateData - Data to update - * @param {string} [updateData.name] - Category name - * @param {string} [updateData.slug] - Category slug - * @param {string} [updateData.description] - Category description - * @param {string} [updateData.parentId] - Parent category ID - * @returns {Promise} Updated category object - */ -export const updateCategory = async (id, updateData) => { - try { - const { name, slug, description, parentId } = updateData; - - // Check if category exists - const existingCategory = await prisma.category.findUnique({ - where: { id } - }); - if (!existingCategory) { - throw new Error('Category not found'); - } - - // If slug is being updated, check if new slug already exists - if (slug && slug !== existingCategory.slug) { - const slugExists = await prisma.category.findUnique({ - where: { slug } - }); - if (slugExists) { - const err = new Error('Category with this slug already exists'); - err.name = 'BadRequestError'; - err.status = 400; - throw err; - } - } - - // If parentId is being updated, validate that parent exists and prevent circular references - if (parentId && parentId !== existingCategory.parentId) { - if (parentId === id) { - throw new Error('Category cannot be its own parent'); - } - - const parentCategory = await prisma.category.findUnique({ - where: { id: parentId } - }); - if (!parentCategory) { - throw new Error('Parent category not found'); - } - - // Check for circular reference by checking if the current category is an ancestor of the new parent - const isCircularReference = await checkCircularReference(id, parentId); - if (isCircularReference) { - throw new Error('Cannot create circular reference in category hierarchy'); - } - } - - // Update the category - const updatedCategory = await prisma.category.update({ - where: { id }, - data: { - ...(name !== undefined && { name }), - ...(slug !== undefined && { slug }), - ...(description !== undefined && { description }), - ...(parentId !== undefined && { parentId }) - }, - include: { - parent: { - select: { - id: true, - name: true, - slug: true - } - }, - children: { - select: { - id: true, - name: true, - slug: true - } - }, - _count: { - select: { - services: true - } - } - } - }); - - return updatedCategory; - } catch (error) { - throw new Error(`Failed to update category: ${error.message}`); - } -}; - -/** - * Delete a category - * @param {string} id - Category ID - * @param {Object} options - Delete options - * @param {boolean} [options.force=false] - Force delete even if category has children or services - * @returns {Promise} Deleted category object - */ -export const deleteCategory = async (id, options = {}) => { - try { - const { force = false } = options; - - // Check if category exists - const existingCategory = await prisma.category.findUnique({ - where: { id }, - include: { - children: true, - _count: { - select: { - services: true - } - } - } - }); - - if (!existingCategory) { - throw new Error('Category not found'); - } - - // Check if category has children or services and force is not enabled - if (!force) { - if (existingCategory.children.length > 0) { - throw new Error('Cannot delete category with child categories. Use force option or delete children first.'); - } - if (existingCategory._count.services > 0) { - throw new Error('Cannot delete category with associated services. Use force option or remove services first.'); - } - } - - // If force delete, handle children and services - if (force) { - // Set children's parentId to null (make them root categories) - await prisma.category.updateMany({ - where: { parentId: id }, - data: { parentId: null } - }); - - // Note: Services will be orphaned but not deleted - // You might want to handle this differently based on business requirements - } - - // Delete the category - const deletedCategory = await prisma.category.delete({ - where: { id } - }); - - return deletedCategory; - } catch (error) { - throw new Error(`Failed to delete category: ${error.message}`); - } -}; - -/** - * Get root categories (categories with no parent) - * @param {Object} options - Additional options - * @param {boolean} [options.includeChildren=true] - Include children categories - * @returns {Promise} Array of root category objects - */ -export const getRootCategories = async (options = {}) => { - try { - const { includeChildren = true } = options; - - return await getAllCategories({ - parentId: null, - includeChildren, - includeParent: false, - includeServices: true - }); - } catch (error) { - throw new Error(`Failed to fetch root categories: ${error.message}`); - } -}; - -/** - * Get category hierarchy starting from a specific category - * @param {string} categoryId - Starting category ID - * @returns {Promise} Category with full hierarchy - */ -export const getCategoryHierarchy = async (categoryId) => { - try { - const category = await prisma.category.findUnique({ - where: { id: categoryId }, - include: { - parent: { - include: { - parent: { - include: { - parent: true // Up to 3 levels up - } - } - } - }, - children: { - include: { - children: { - include: { - children: true // Up to 3 levels down - } - } - } - } - } - }); - - return category; - } catch (error) { - throw new Error(`Failed to fetch category hierarchy: ${error.message}`); - } -}; - -/** - * Helper function to check for circular references in category hierarchy - * @param {string} categoryId - Current category ID - * @param {string} newParentId - New parent category ID - * @returns {Promise} True if circular reference would be created - */ -const checkCircularReference = async (categoryId, newParentId) => { - let currentParentId = newParentId; - - while (currentParentId) { - if (currentParentId === categoryId) { - return true; // Circular reference found - } - - const parent = await prisma.category.findUnique({ - where: { id: currentParentId }, - select: { parentId: true } - }); - - if (!parent) break; - currentParentId = parent.parentId; - } - - return false; -}; - -/** - * Search categories by name or description - * @param {string} searchTerm - Search term - * @param {Object} options - Search options - * @param {boolean} [options.includeChildren=true] - Include children categories - * @param {boolean} [options.includeParent=true] - Include parent category info - * @returns {Promise} Array of matching categories - */ -export const searchCategories = async (searchTerm, options = {}) => { - try { - const { includeChildren = true, includeParent = true } = options; - - const categories = await prisma.category.findMany({ - where: { - OR: [ - { name: { contains: searchTerm, mode: 'insensitive' } }, - { description: { contains: searchTerm, mode: 'insensitive' } } - ] - }, - include: { - parent: includeParent ? { - select: { - id: true, - name: true, - slug: true - } - } : false, - children: includeChildren ? { - select: { - id: true, - name: true, - slug: true, - description: true - } - } : false, - _count: { - select: { - services: true - } - } - }, - orderBy: { - name: 'asc' - } - }); - - return categories; - } catch (error) { - throw new Error(`Failed to search categories: ${error.message}`); - } -}; diff --git a/src/services/comment.service.ts b/src/services/comment.service.ts new file mode 100755 index 0000000..6673575 --- /dev/null +++ b/src/services/comment.service.ts @@ -0,0 +1,203 @@ +import { prisma } from '../utils/database'; +import { ActivityType, EntityType } from '@prisma/client'; +import { createActivity } from './activity.service'; + +export interface CreateCommentData { + content: string; + taskId: string; +} + +export interface UpdateCommentData { + content: string; +} + +export const createComment = async (userId: string, commentData: CreateCommentData) => { + // Check if user has access to the task + const task = await prisma.task.findFirst({ + where: { + id: commentData.taskId, + OR: [ + { createdById: userId }, + { assignments: { some: { userId } } }, + { team: { members: { some: { userId } } } } + ] + }, + select: { + id: true, + title: true, + teamId: true + } + }); + + if (!task) { + throw new Error('Task not found or access denied'); + } + + const comment = await prisma.comment.create({ + data: { + content: commentData.content, + taskId: commentData.taskId, + userId + }, + include: { + user: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + }, + task: { + select: { + id: true, + title: true + } + } + } + }); + + // Create activity + await createActivity({ + type: ActivityType.TASK_COMMENTED, + description: `Comment added to task "${task.title}"`, + entityType: EntityType.TASK, + entityId: task.id, + userId, + teamId: task.teamId || undefined, + metadata: { + taskTitle: task.title, + commentId: comment.id + } + }); + + return comment; +}; + +export const getTaskComments = async (taskId: string, userId: string, page = 1, limit = 10) => { + // Check if user has access to the task + const task = await prisma.task.findFirst({ + where: { + id: taskId, + OR: [ + { createdById: userId }, + { assignments: { some: { userId } } }, + { team: { members: { some: { userId } } } } + ] + } + }); + + if (!task) { + throw new Error('Task not found or access denied'); + } + + const skip = (page - 1) * limit; + + const [comments, total] = await Promise.all([ + prisma.comment.findMany({ + where: { taskId }, + skip, + take: limit, + orderBy: { createdAt: 'desc' }, + include: { + user: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + } + } + }), + prisma.comment.count({ where: { taskId } }) + ]); + + return { + comments, + pagination: { + current: page, + total: Math.ceil(total / limit), + count: comments.length, + totalCount: total + } + }; +}; + +export const updateComment = async (commentId: string, userId: string, updateData: UpdateCommentData) => { + // Check if user owns the comment + const existingComment = await prisma.comment.findFirst({ + where: { + id: commentId, + userId + } + }); + + if (!existingComment) { + throw new Error('Comment not found or access denied'); + } + + const comment = await prisma.comment.update({ + where: { id: commentId }, + data: updateData, + include: { + user: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + }, + task: { + select: { + id: true, + title: true + } + } + } + }); + + return comment; +}; + +export const deleteComment = async (commentId: string, userId: string) => { + // Check if user owns the comment or has admin permissions on the task's team + const comment = await prisma.comment.findFirst({ + where: { id: commentId }, + include: { + task: { + include: { + team: { + include: { + members: { + where: { userId } + } + } + } + } + } + } + }); + + if (!comment) { + throw new Error('Comment not found'); + } + + // User can delete if they own the comment or are team admin/owner + const canDelete = comment.userId === userId || + (comment.task.team && + comment.task.team.members.length > 0 && + comment.task.team.members[0] && + ['OWNER', 'ADMIN'].includes(comment.task.team.members[0].role)); + + if (!canDelete) { + throw new Error('Access denied'); + } + + await prisma.comment.delete({ + where: { id: commentId } + }); + + return { message: 'Comment deleted successfully' }; +}; \ No newline at end of file diff --git a/src/services/company.service.js b/src/services/company.service.js deleted file mode 100644 index d44648b..0000000 --- a/src/services/company.service.js +++ /dev/null @@ -1,107 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -export const createCompany = async (userId, companyData) => { - // Check if user is a verified provider - const provider = await prisma.serviceProvider.findUnique({ - where: { userId }, - select: { id: true, isVerified: true } - }); - - if (!provider) { - throw new Error('Provider profile not found'); - } - - if (!provider.isVerified) { - throw new Error('Only verified providers can create companies'); - } - - const company = await prisma.company.create({ - data: { - providerId: provider.id, - name: companyData.name, - description: companyData.description, - logo: companyData.logo, - address: companyData.address, - contact: companyData.contact, - socialmedia: companyData.socialmedia || [] - } - }); - - return company; -}; - -export const updateCompany = async (userId, companyId, companyData) => { - // Check if user owns this company - const provider = await prisma.serviceProvider.findUnique({ - where: { userId }, - include: { - companies: { - where: { id: companyId } - } - } - }); - - if (!provider) { - throw new Error('Provider profile not found'); - } - - if (provider.companies.length === 0) { - throw new Error('Company not found or you do not have permission to update it'); - } - - const updatedData = {}; - if (companyData.name !== undefined) updatedData.name = companyData.name; - if (companyData.description !== undefined) updatedData.description = companyData.description; - if (companyData.logo !== undefined) updatedData.logo = companyData.logo; - if (companyData.address !== undefined) updatedData.address = companyData.address; - if (companyData.contact !== undefined) updatedData.contact = companyData.contact; - if (companyData.socialmedia !== undefined) updatedData.socialmedia = companyData.socialmedia; - - const company = await prisma.company.update({ - where: { id: companyId }, - data: updatedData - }); - - return company; -}; - -export const deleteCompany = async (userId, companyId) => { - // Check if user owns this company - const provider = await prisma.serviceProvider.findUnique({ - where: { userId }, - include: { - companies: { - where: { id: companyId } - } - } - }); - - if (!provider) { - throw new Error('Provider profile not found'); - } - - if (provider.companies.length === 0) { - throw new Error('Company not found or you do not have permission to delete it'); - } - - await prisma.company.delete({ - where: { id: companyId } - }); -}; - -export const getCompanies = async (userId) => { - const provider = await prisma.serviceProvider.findUnique({ - where: { userId }, - include: { - companies: true - } - }); - - if (!provider) { - throw new Error('Provider profile not found'); - } - - return provider.companies; -}; diff --git a/src/services/dependency.service.ts b/src/services/dependency.service.ts new file mode 100755 index 0000000..635bc3c --- /dev/null +++ b/src/services/dependency.service.ts @@ -0,0 +1,344 @@ +import { prisma } from '../utils/database'; +import { ActivityType, EntityType } from '@prisma/client'; +import { createActivity } from './activity.service'; + +export interface CreateDependencyData { + taskId: string; + dependsOnTaskId: string; +} + +export const addTaskDependency = async (userId: string, dependencyData: CreateDependencyData) => { + const { taskId, dependsOnTaskId } = dependencyData; + + if (taskId === dependsOnTaskId) { + throw new Error('A task cannot depend on itself'); + } + + // Check if user has permission to modify the task + const task = await prisma.task.findFirst({ + where: { + id: taskId, + OR: [ + { createdById: userId }, + { team: { members: { some: { userId, role: { in: ['OWNER', 'ADMIN'] } } } } } + ] + } + }); + + if (!task) { + throw new Error('Task not found or insufficient permissions'); + } + + // Check if dependency task exists and user has access + const dependencyTask = await prisma.task.findFirst({ + where: { + id: dependsOnTaskId, + OR: [ + { createdById: userId }, + { assignments: { some: { userId } } }, + { team: { members: { some: { userId } } } } + ] + } + }); + + if (!dependencyTask) { + throw new Error('Dependency task not found or access denied'); + } + + // Check if dependency already exists + const existingDependency = await prisma.taskDependency.findFirst({ + where: { + taskId, + dependsOnTaskId + } + }); + + if (existingDependency) { + throw new Error('Dependency already exists'); + } + + // Check for circular dependencies + const wouldCreateCircularDependency = await checkCircularDependency(dependsOnTaskId, taskId); + if (wouldCreateCircularDependency) { + throw new Error('This dependency would create a circular dependency'); + } + + // Create the dependency + const dependency = await prisma.taskDependency.create({ + data: { + taskId, + dependsOnTaskId + }, + include: { + task: { + select: { + id: true, + title: true + } + }, + dependsOnTask: { + select: { + id: true, + title: true, + status: true + } + } + } + }); + + // Create activity + await createActivity({ + type: ActivityType.TASK_UPDATED, + description: `Task "${task.title}" now depends on "${dependencyTask.title}"`, + entityType: EntityType.TASK, + entityId: taskId, + userId, + teamId: task.teamId || undefined, + metadata: { + action: 'dependency_added', + dependencyTaskId: dependsOnTaskId, + dependencyTaskTitle: dependencyTask.title + } + }); + + return dependency; +}; + +export const removeTaskDependency = async (userId: string, dependencyId: string) => { + // Find the dependency and check permissions + const dependency = await prisma.taskDependency.findFirst({ + where: { id: dependencyId }, + include: { + task: { + include: { + team: { + include: { + members: { + where: { userId } + } + } + } + } + } + } + }); + + if (!dependency) { + throw new Error('Dependency not found'); + } + + // Check permissions + const hasPermission = dependency.task.createdById === userId || + (dependency.task.team && + dependency.task.team.members.length > 0 && + dependency.task.team.members[0] && + ['OWNER', 'ADMIN'].includes(dependency.task.team.members[0].role)); + + if (!hasPermission) { + throw new Error('Insufficient permissions'); + } + + await prisma.taskDependency.delete({ + where: { id: dependencyId } + }); + + // Create activity + await createActivity({ + type: ActivityType.TASK_UPDATED, + description: `Task dependency removed from "${dependency.task.title}"`, + entityType: EntityType.TASK, + entityId: dependency.task.id, + userId, + teamId: dependency.task.teamId || undefined, + metadata: { + action: 'dependency_removed', + dependencyId + } + }); + + return { message: 'Dependency removed successfully' }; +}; + +export const getTaskDependencies = async (taskId: string, userId: string) => { + // Check if user has access to the task + const task = await prisma.task.findFirst({ + where: { + id: taskId, + OR: [ + { createdById: userId }, + { assignments: { some: { userId } } }, + { team: { members: { some: { userId } } } } + ] + } + }); + + if (!task) { + throw new Error('Task not found or access denied'); + } + + const [dependencies, dependents] = await Promise.all([ + // Tasks this task depends on + prisma.taskDependency.findMany({ + where: { taskId }, + include: { + dependsOnTask: { + select: { + id: true, + title: true, + status: true, + priority: true, + dueDate: true + } + } + } + }), + // Tasks that depend on this task + prisma.taskDependency.findMany({ + where: { dependsOnTaskId: taskId }, + include: { + task: { + select: { + id: true, + title: true, + status: true, + priority: true, + dueDate: true + } + } + } + }) + ]); + + return { + dependencies: dependencies.map(d => d.dependsOnTask), + dependents: dependents.map(d => d.task) + }; +}; + +export const validateTaskCanBeCompleted = async (taskId: string, userId: string) => { + // Check if user has access to the task + const task = await prisma.task.findFirst({ + where: { + id: taskId, + OR: [ + { createdById: userId }, + { assignments: { some: { userId } } }, + { team: { members: { some: { userId } } } } + ] + } + }); + + if (!task) { + throw new Error('Task not found or access denied'); + } + + // Get all dependencies + const dependencies = await prisma.taskDependency.findMany({ + where: { taskId }, + include: { + dependsOnTask: { + select: { + id: true, + title: true, + status: true + } + } + } + }); + + // Check if all dependencies are completed + const incompleteDependencies = dependencies.filter( + d => d.dependsOnTask.status !== 'DONE' + ); + + if (incompleteDependencies.length > 0) { + return { + canComplete: false, + blockedBy: incompleteDependencies.map(d => ({ + id: d.dependsOnTask.id, + title: d.dependsOnTask.title, + status: d.dependsOnTask.status + })) + }; + } + + return { + canComplete: true, + blockedBy: [] + }; +}; + +// Helper function to check for circular dependencies using DFS +const checkCircularDependency = async (startTaskId: string, targetTaskId: string): Promise => { + const visited = new Set(); + const recursionStack = new Set(); + + const hasCycle = async (taskId: string): Promise => { + if (recursionStack.has(taskId)) { + return taskId === targetTaskId; + } + + if (visited.has(taskId)) { + return false; + } + + visited.add(taskId); + recursionStack.add(taskId); + + // Get all tasks that this task depends on + const dependencies = await prisma.taskDependency.findMany({ + where: { taskId }, + select: { dependsOnTaskId: true } + }); + + for (const dep of dependencies) { + if (await hasCycle(dep.dependsOnTaskId)) { + return true; + } + } + + recursionStack.delete(taskId); + return false; + }; + + return await hasCycle(startTaskId); +}; + +export const getDependencyGraph = async (userId: string, teamId?: string) => { + const whereClause: any = { + OR: [ + { createdById: userId }, + { assignments: { some: { userId } } }, + { team: { members: { some: { userId } } } } + ] + }; + + if (teamId) { + whereClause.teamId = teamId; + } + + const tasks = await prisma.task.findMany({ + where: whereClause, + select: { + id: true, + title: true, + status: true, + priority: true + } + }); + + const dependencies = await prisma.taskDependency.findMany({ + where: { + task: whereClause + }, + select: { + taskId: true, + dependsOnTaskId: true + } + }); + + return { + tasks, + dependencies + }; +}; \ No newline at end of file diff --git a/src/services/googleMaps.service.ts b/src/services/googleMaps.service.ts new file mode 100755 index 0000000..5015ff7 --- /dev/null +++ b/src/services/googleMaps.service.ts @@ -0,0 +1,218 @@ +import { Client } from '@googlemaps/google-maps-services-js'; +import { config } from 'dotenv'; + +config(); + +interface LocationData { + lat: number; + lng: number; + formatted_address: string; + city: string; + state: string; + country: string; + postal_code: string; +} + +interface AddressComponent { + long_name: string; + short_name: string; + types: string[]; +} + +class GoogleMapsService { + private client: Client; + private apiKey: string; + private geocodeCache: Map; + + constructor() { + this.client = new Client({}); + this.apiKey = process.env.GOOGLE_MAPS_API_KEY || ''; + this.geocodeCache = new Map(); + + if (!this.apiKey) { + console.warn('Google Maps API key not found. Location services will be disabled.'); + } + } + + /** + * Convert address to coordinates using Google Geocoding API + */ + async geocodeAddress(address: string): Promise { + if (!this.apiKey) { + throw new Error('Google Maps API key not configured'); + } + + // Check cache first + if (this.geocodeCache.has(address)) { + return this.geocodeCache.get(address)!; + } + + try { + const response = await this.client.geocode({ + params: { + address: address, + key: this.apiKey, + }, + }); + + if (response.data.results.length === 0) { + throw new Error('Address not found'); + } + + const result = response.data.results[0]; + const location = result.geometry.location; + const components = result.address_components; + + const locationData: LocationData = { + lat: location.lat, + lng: location.lng, + formatted_address: result.formatted_address, + city: this.extractComponent(components, 'locality') || + this.extractComponent(components, 'administrative_area_level_2') || '', + state: this.extractComponent(components, 'administrative_area_level_1') || '', + country: this.extractComponent(components, 'country') || '', + postal_code: this.extractComponent(components, 'postal_code') || '', + }; + + // Cache the result + this.geocodeCache.set(address, locationData); + + return locationData; + } catch (error) { + console.error('Geocoding error:', error); + throw new Error(`Failed to geocode address: ${address}`); + } + } + + /** + * Convert coordinates to address using Google Reverse Geocoding API + */ + async reverseGeocode(lat: number, lng: number): Promise { + if (!this.apiKey) { + throw new Error('Google Maps API key not configured'); + } + + const cacheKey = `${lat},${lng}`; + if (this.geocodeCache.has(cacheKey)) { + return this.geocodeCache.get(cacheKey)!; + } + + try { + const response = await this.client.reverseGeocode({ + params: { + latlng: { lat, lng }, + key: this.apiKey, + }, + }); + + if (response.data.results.length === 0) { + throw new Error('Location not found'); + } + + const result = response.data.results[0]; + const components = result.address_components; + + const locationData: LocationData = { + lat, + lng, + formatted_address: result.formatted_address, + city: this.extractComponent(components, 'locality') || + this.extractComponent(components, 'administrative_area_level_2') || '', + state: this.extractComponent(components, 'administrative_area_level_1') || '', + country: this.extractComponent(components, 'country') || '', + postal_code: this.extractComponent(components, 'postal_code') || '', + }; + + // Cache the result + this.geocodeCache.set(cacheKey, locationData); + + return locationData; + } catch (error) { + console.error('Reverse geocoding error:', error); + throw new Error(`Failed to reverse geocode coordinates: ${lat}, ${lng}`); + } + } + + /** + * Validate coordinates + */ + validateCoordinates(lat: number, lng: number): boolean { + return lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180; + } + + /** + * Calculate distance between two points in kilometers + */ + calculateDistance(lat1: number, lng1: number, lat2: number, lng2: number): number { + const R = 6371; // Earth's radius in kilometers + const dLat = this.toRadians(lat2 - lat1); + const dLng = this.toRadians(lng2 - lng1); + + const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(this.toRadians(lat1)) * Math.cos(this.toRadians(lat2)) * + Math.sin(dLng / 2) * Math.sin(dLng / 2); + + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return R * c; + } + + /** + * Get location from IP address as fallback + */ + async getLocationFromIP(ip?: string): Promise> { + try { + // Using a free IP geolocation service + const url = ip ? `https://ipapi.co/${ip}/json/` : 'https://ipapi.co/json/'; + const response = await fetch(url); + const data = await response.json() as any; + + if (data.error) { + throw new Error(data.reason || 'IP geolocation failed'); + } + + return { + lat: data.latitude, + lng: data.longitude, + city: data.city || '', + state: data.region || '', + country: data.country_name || '', + postal_code: data.postal || '', + formatted_address: `${data.city}, ${data.region}, ${data.country_name}` + }; + } catch (error) { + console.error('IP geolocation error:', error); + throw new Error('Could not determine location from IP'); + } + } + + /** + * Extract address component by type + */ + private extractComponent(components: AddressComponent[], type: string): string { + const component = components.find(c => c.types.includes(type)); + return component ? component.long_name : ''; + } + + /** + * Convert degrees to radians + */ + private toRadians(degrees: number): number { + return degrees * (Math.PI / 180); + } + + /** + * Clear geocoding cache + */ + clearCache(): void { + this.geocodeCache.clear(); + } + + /** + * Get cache size + */ + getCacheSize(): number { + return this.geocodeCache.size; + } +} + +export default new GoogleMapsService(); \ No newline at end of file diff --git a/src/services/provider.service.js b/src/services/provider.service.js deleted file mode 100644 index f2f3b5f..0000000 --- a/src/services/provider.service.js +++ /dev/null @@ -1,228 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -export const createProvider = async (userId, providerData) => { - // Check if user exists and doesn't already have a provider profile - const user = await prisma.user.findUnique({ - where: { id: userId }, - include: { serviceProvider: true } - }); - - if (!user) { - throw new Error('User not found'); - } - - if (user.serviceProvider) { - throw new Error('User already has a service provider profile'); - } - - // Update user role to PROVIDER and create ServiceProvider profile - const [updatedUser, newProvider] = await prisma.$transaction([ - prisma.user.update({ - where: { id: userId }, - data: { role: 'PROVIDER' } - }), - prisma.serviceProvider.create({ - data: { - userId, - bio: providerData.bio, - skills: providerData.skills || [], - qualifications: providerData.qualifications || [], - logoUrl: providerData.logoUrl, - IDCardUrl: providerData.IDCardUrl, - }, - include: { - user: { - select: { - id: true, - email: true, - firstName: true, - lastName: true, - imageUrl: true, - role: true - } - } - } - }) - ]); - - return newProvider; -}; - -export const updateProvider = async (userId, providerData) => { - // Check if user has a provider profile - const existingProvider = await prisma.serviceProvider.findUnique({ - where: { userId } - }); - - if (!existingProvider) { - throw new Error('Service provider profile not found'); - } - - const updatedData = {}; - if (providerData.bio !== undefined) updatedData.bio = providerData.bio; - if (providerData.skills !== undefined) updatedData.skills = providerData.skills; - if (providerData.qualifications !== undefined) updatedData.qualifications = providerData.qualifications; - if (providerData.logoUrl !== undefined) updatedData.logoUrl = providerData.logoUrl; - if (providerData.IDCardUrl !== undefined) updatedData.IDCardUrl = providerData.IDCardUrl; - - const updatedProvider = await prisma.serviceProvider.update({ - where: { userId }, - data: updatedData, - include: { - user: { - select: { - id: true, - email: true, - firstName: true, - lastName: true, - imageUrl: true, - role: true - } - }, - services: { - select: { - id: true, - title: true, - description: true, - price: true, - currency: true, - images: true, - isActive: true - } - }, - reviews: { - select: { - id: true, - rating: true, - comment: true, - createdAt: true, - reviewer: { - select: { - firstName: true, - lastName: true, - imageUrl: true - } - } - }, - orderBy: { - createdAt: 'desc' - }, - take: 10 - } - } - }); - - return updatedProvider; -}; - -export const deleteProvider = async (userId) => { - // Check if user has a provider profile - const existingProvider = await prisma.serviceProvider.findUnique({ - where: { userId }, - include: { - services: true, - schedules: true, - payments: true, - reviews: true - } - }); - - if (!existingProvider) { - throw new Error('Service provider profile not found'); - } - - // Check for active dependencies - const activeServices = existingProvider.services.filter(service => service.isActive); - if (activeServices.length > 0) { - throw new Error('Cannot delete provider with active services. Please deactivate all services first.'); - } - - const pendingSchedules = existingProvider.schedules.filter(schedule => !schedule.confirm); - if (pendingSchedules.length > 0) { - throw new Error('Cannot delete provider with pending schedules.'); - } - - // Delete provider and update user role back to USER - await prisma.$transaction([ - prisma.serviceProvider.delete({ - where: { userId } - }), - prisma.user.update({ - where: { id: userId }, - data: { role: 'USER' } - }) - ]); - - return { message: 'Service provider profile deleted successfully' }; -}; - -export const getProviderProfile = async (userId) => { - const provider = await prisma.serviceProvider.findUnique({ - where: { userId }, - include: { - user: { - select: { - id: true, - email: true, - firstName: true, - lastName: true, - imageUrl: true, - role: true, - phone: true, - location: true, - address: true, - socialmedia: true, - createdAt: true, - isEmailVerified: true - } - }, - companies: { - orderBy: { - id: 'desc' - } - }, - services: { - select: { - id: true, - title: true, - description: true, - price: true, - currency: true, - images: true, - isActive: true, - tags: true, - createdAt: true - }, - orderBy: { - createdAt: 'desc' - } - }, - reviews: { - select: { - id: true, - rating: true, - comment: true, - createdAt: true, - reviewer: { - select: { - firstName: true, - lastName: true, - imageUrl: true - } - } - }, - orderBy: { - createdAt: 'desc' - } - } - } - }); - - if (!provider) { - throw new Error('Service provider profile not found'); - } - - return provider; -}; diff --git a/src/services/semantic-search.service.ts b/src/services/semantic-search.service.ts new file mode 100755 index 0000000..0a5fee2 --- /dev/null +++ b/src/services/semantic-search.service.ts @@ -0,0 +1,329 @@ +import { prisma } from '../utils/database.js'; +import { embeddingService } from './embedding.service.js'; + +export interface SemanticSearchOptions { + query: string; + limit?: number; + threshold?: number; + categoryId?: string; + providerId?: string; + isActive?: boolean; + minPrice?: number; + maxPrice?: number; +} + +export interface SemanticSearchResult { + id: string; + title: string; + description: string; + price: number; + currency: string; + tags: string[]; + images: string[]; + similarity: number; + provider: { + id: string; + user: { + firstName: string; + lastName: string; + }; + }; + category: { + id: string; + name: string; + }; +} + +export class SemanticSearchService { + /** + * Perform semantic search for services + */ + async searchServices(options: SemanticSearchOptions): Promise { + const { + query, + limit = 20, + threshold = 0.3, + categoryId, + providerId, + isActive = true, + minPrice, + maxPrice + } = options; + + try { + // Generate embedding for search query + const queryEmbedding = await embeddingService.generateEmbedding(query); + + // Convert embedding to PostgreSQL array format + const embeddingVector = `[${queryEmbedding.join(',')}]`; + + // Build WHERE conditions + const whereConditions = ['s."isActive" = $1']; + const params: any[] = [isActive]; + let paramIndex = 2; + + if (categoryId) { + whereConditions.push(`s."categoryId" = $${paramIndex}`); + params.push(categoryId); + paramIndex++; + } + + if (providerId) { + whereConditions.push(`s."providerId" = $${paramIndex}`); + params.push(providerId); + paramIndex++; + } + + if (minPrice !== undefined) { + whereConditions.push(`s.price >= $${paramIndex}`); + params.push(minPrice); + paramIndex++; + } + + if (maxPrice !== undefined) { + whereConditions.push(`s.price <= $${paramIndex}`); + params.push(maxPrice); + paramIndex++; + } + + // Add similarity threshold + whereConditions.push(`(1 - (s."combinedEmbedding" <=> $${paramIndex}::vector)) >= $${paramIndex + 1}`); + params.push(embeddingVector, threshold); + paramIndex += 2; + + const whereClause = whereConditions.join(' AND '); + + // Raw SQL query for vector similarity search + const query_sql = ` + SELECT + s.id, + s.title, + s.description, + s.price, + s.currency, + s.tags, + s.images, + s."createdAt", + (1 - (s."combinedEmbedding" <=> $${paramIndex}::vector)) as similarity, + p.id as provider_id, + u."firstName" as provider_first_name, + u."lastName" as provider_last_name, + c.id as category_id, + c.name as category_name + FROM "Service" s + INNER JOIN "ServiceProvider" p ON s."providerId" = p.id + INNER JOIN "User" u ON p."userId" = u.id + INNER JOIN "Category" c ON s."categoryId" = c.id + WHERE ${whereClause} + AND s."combinedEmbedding" IS NOT NULL + ORDER BY similarity DESC + LIMIT $${paramIndex + 1} + `; + + params.push(embeddingVector, limit); + + const results = await prisma.$queryRawUnsafe(query_sql, ...params) as any[]; + + return results.map(row => ({ + id: row.id, + title: row.title || '', + description: row.description || '', + price: parseFloat(row.price), + currency: row.currency, + tags: row.tags || [], + images: row.images || [], + similarity: parseFloat(row.similarity), + provider: { + id: row.provider_id, + user: { + firstName: row.provider_first_name || '', + lastName: row.provider_last_name || '' + } + }, + category: { + id: row.category_id, + name: row.category_name || '' + } + })); + + } catch (error) { + console.error('Semantic search error:', error); + throw new Error('Failed to perform semantic search'); + } + } + + /** + * Update embeddings for a service + */ + async updateServiceEmbeddings(serviceId: string) { + try { + // Get service data + const service = await prisma.service.findUnique({ + where: { id: serviceId }, + select: { + id: true, + title: true, + description: true, + tags: true + } + }); + + if (!service) { + throw new Error(`Service with ID ${serviceId} not found`); + } + + // Generate embeddings + const embeddings = await embeddingService.generateServiceEmbeddings({ + title: service.title, + description: service.description, + tags: service.tags + }); + + // Update service with embeddings using raw query + await prisma.$executeRaw` + UPDATE "Service" + SET + "titleEmbedding" = ${`[${embeddings.titleEmbedding.join(',')}]`}::vector, + "descriptionEmbedding" = ${`[${embeddings.descriptionEmbedding.join(',')}]`}::vector, + "tagsEmbedding" = ${`[${embeddings.tagsEmbedding.join(',')}]`}::vector, + "combinedEmbedding" = ${`[${embeddings.combinedEmbedding.join(',')}]`}::vector, + "embeddingUpdatedAt" = NOW() + WHERE id = ${serviceId} + `; + + console.log(`✅ Updated embeddings for service ${serviceId}`); + + } catch (error) { + console.error(`❌ Failed to update embeddings for service ${serviceId}:`, error); + throw error; + } + } + + /** + * Batch update embeddings for all services that don't have them + */ + async updateAllServiceEmbeddings(batchSize: number = 5) { // Reduced batch size for free tier + try { + console.log('🚀 Starting batch embedding update...'); + + // Get services without embeddings (smaller batches for free tier) + const services = await prisma.$queryRawUnsafe(` + SELECT id, title, description, tags + FROM "Service" + WHERE "combinedEmbedding" IS NULL + AND "isActive" = true + LIMIT $1 + `, batchSize) as any[]; + + console.log(`📝 Found ${services.length} services to update`); + + for (let i = 0; i < services.length; i++) { + const service = services[i]; + try { + console.log(`âŗ Processing service ${i + 1}/${services.length}: ${service.title}`); + await this.updateServiceEmbeddings(service.id); + + // Add delay between requests for free tier (4+ seconds) + if (i < services.length - 1) { + console.log('âąī¸ Waiting 5s before next request (rate limit)...'); + await new Promise(resolve => setTimeout(resolve, 5000)); + } + } catch (error) { + console.error(`Failed to update embeddings for service ${service.id}:`, error); + // Continue with next service + } + } + + console.log('✅ Batch embedding update completed'); + return services.length; + + } catch (error) { + console.error('❌ Batch embedding update failed:', error); + throw error; + } + } + + /** + * Find similar services to a given service + */ + async findSimilarServices(serviceId: string, limit: number = 5): Promise { + try { + console.log('🔍 Finding similar services for:', serviceId); + + // Use raw query to get embedding (cast to text to avoid deserialization issues) + const serviceResult = await prisma.$queryRawUnsafe(` + SELECT "combinedEmbedding"::text as embedding_text + FROM "Service" + WHERE id = $1 AND "combinedEmbedding" IS NOT NULL + `, serviceId) as any[]; + + console.log('📊 Service query result:', serviceResult?.length ? 'Found embedding' : 'No embedding found'); + + if (!serviceResult || serviceResult.length === 0) { + throw new Error('Service embedding not found'); + } + + const serviceEmbedding = serviceResult[0].embedding_text; + console.log('✅ Got service embedding as text, length:', serviceEmbedding?.length || 0); + + const query_sql = ` + SELECT + s.id, + s.title, + s.description, + s.price, + s.currency, + s.tags, + s.images, + (1 - (s."combinedEmbedding" <=> ($1::text)::vector)) as similarity, + p.id as provider_id, + u."firstName" as provider_first_name, + u."lastName" as provider_last_name, + c.id as category_id, + c.name as category_name + FROM "Service" s + INNER JOIN "ServiceProvider" p ON s."providerId" = p.id + INNER JOIN "User" u ON p."userId" = u.id + INNER JOIN "Category" c ON s."categoryId" = c.id + WHERE s.id != $2 + AND s."isActive" = true + AND s."combinedEmbedding" IS NOT NULL + ORDER BY similarity DESC + LIMIT $3 + `; + + console.log('🔍 Executing similarity query with params:', { serviceId, limit }); + const results = await prisma.$queryRawUnsafe(query_sql, serviceEmbedding, serviceId, limit) as any[]; + console.log('📊 Query results count:', results?.length || 0); + + return results.map(row => ({ + id: row.id, + title: row.title || '', + description: row.description || '', + price: parseFloat(row.price), + currency: row.currency, + tags: row.tags || [], + images: row.images || [], + similarity: parseFloat(row.similarity), + provider: { + id: row.provider_id, + user: { + firstName: row.provider_first_name || '', + lastName: row.provider_last_name || '' + } + }, + category: { + id: row.category_id, + name: row.category_name || '' + } + })); + + } catch (error) { + console.error('❌ Error finding similar services:', error); + console.error('Error details:', error.message, error.stack); + throw new Error('Failed to find similar services'); + } + } +} + +export const semanticSearchService = new SemanticSearchService(); diff --git a/src/services/services.service.js b/src/services/services.service.js deleted file mode 100644 index 244f42e..0000000 --- a/src/services/services.service.js +++ /dev/null @@ -1,271 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -/** - * Create a new service - * @param {Object} serviceData - The service data - * @param {string} serviceData.providerId - ID of the service provider - * @param {string} serviceData.categoryId - ID of the service category - * @param {string} [serviceData.title] - Service title - * @param {string} [serviceData.description] - Service description - * @param {number} serviceData.price - Service price - * @param {string} [serviceData.currency="USD"] - Currency (defaults to USD) - * @param {string[]} [serviceData.tags=[]] - Array of tags - * @param {string[]} [serviceData.images=[]] - Array of image URLs - * @param {boolean} [serviceData.isActive=true] - Service active status - * @param {string[]} [serviceData.workingTime=[]] - Array of working time slots - * @returns {Promise} Created service object - */ -export const createService = async (serviceData) => { - try { - const { - providerId, - categoryId, - title, - description, - price, - currency = "USD", - tags = [], - images = [], - isActive = true, - workingTime = [] - } = serviceData; - - // Validate required fields - if (!providerId) { - throw new Error('Provider ID is required'); - } - if (!categoryId) { - throw new Error('Category ID is required'); - } - if (price === undefined || price === null) { - throw new Error('Price is required'); - } - - // Validate that provider exists - const provider = await prisma.serviceProvider.findUnique({ - where: { id: providerId } - }); - if (!provider) { - throw new Error('Service provider not found'); - } - - // Validate that category exists - const category = await prisma.category.findUnique({ - where: { id: categoryId } - }); - if (!category) { - throw new Error('Category not found'); - } - - // Create the service - const newService = await prisma.service.create({ - data: { - providerId, - categoryId, - title, - description, - price, - currency, - tags, - images, - isActive, - workingTime - }, - include: { - provider: { - include: { - user: { - select: { - firstName: true, - lastName: true, - email: true - } - } - } - }, - category: true - } - }); - - return newService; - } catch (error) { - throw new Error(`Failed to create service: ${error.message}`); - } -}; - -/** - * Get all services with optional filtering - * @param {Object} filters - Optional filters - * @param {string} [filters.providerId] - Filter by provider ID - * @param {string} [filters.categoryId] - Filter by category ID - * @param {boolean} [filters.isActive] - Filter by active status - * @param {number} [filters.skip=0] - Number of records to skip for pagination - * @param {number} [filters.take=10] - Number of records to take for pagination - * @returns {Promise} Array of service objects - */ -export const getServices = async (filters = {}) => { - try { - const { - providerId, - categoryId, - isActive, - skip = 0, - take = 10 - } = filters; - - const whereClause = {}; - - if (providerId) whereClause.providerId = providerId; - if (categoryId) whereClause.categoryId = categoryId; - if (isActive !== undefined) whereClause.isActive = isActive; - - const services = await prisma.service.findMany({ - where: whereClause, - skip, - take, - include: { - provider: { - include: { - user: { - select: { - firstName: true, - lastName: true, - email: true - } - } - } - }, - category: true, - reviews: { - include: { - reviewer: { - select: { - firstName: true, - lastName: true - } - } - } - } - }, - orderBy: { - createdAt: 'desc' - } - }); - - return services; - } catch (error) { - throw new Error(`Failed to fetch services: ${error.message}`); - } -}; - -/** - * Get a single service by ID - * @param {string} serviceId - The service ID - * @returns {Promise} Service object or null if not found - */ -export const getServiceById = async (serviceId) => { - try { - const service = await prisma.service.findUnique({ - where: { id: serviceId }, - include: { - provider: { - include: { - user: { - select: { - firstName: true, - lastName: true, - email: true, - phone: true - } - } - } - }, - category: true, - reviews: { - include: { - reviewer: { - select: { - firstName: true, - lastName: true - } - } - } - }, - schedules: true - } - }); - - return service; - } catch (error) { - throw new Error(`Failed to fetch service: ${error.message}`); - } -}; - -/** - * Update a service - * @param {string} serviceId - The service ID - * @param {Object} updateData - Data to update - * @returns {Promise} Updated service object - */ -export const updateService = async (serviceId, updateData) => { - try { - const service = await prisma.service.findUnique({ - where: { id: serviceId } - }); - - if (!service) { - throw new Error('Service not found'); - } - - const updatedService = await prisma.service.update({ - where: { id: serviceId }, - data: updateData, - include: { - provider: { - include: { - user: { - select: { - firstName: true, - lastName: true, - email: true - } - } - } - }, - category: true - } - }); - - return updatedService; - } catch (error) { - throw new Error(`Failed to update service: ${error.message}`); - } -}; - -/** - * Delete a service - * @param {string} serviceId - The service ID - * @returns {Promise} Deleted service object - */ -export const deleteService = async (serviceId) => { - try { - const service = await prisma.service.findUnique({ - where: { id: serviceId } - }); - - if (!service) { - throw new Error('Service not found'); - } - - const deletedService = await prisma.service.delete({ - where: { id: serviceId } - }); - - return deletedService; - } catch (error) { - throw new Error(`Failed to delete service: ${error.message}`); - } -}; - diff --git a/src/services/services.service.ts b/src/services/services.service.ts new file mode 100755 index 0000000..5558227 --- /dev/null +++ b/src/services/services.service.ts @@ -0,0 +1,609 @@ +import { prisma } from '../utils/database.js'; +import { embeddingService } from './embedding.service.js'; + +// Type definitions +interface ServiceCreateData { + providerId: string; + categoryId: string; + title?: string; + description?: string; + price: number; + currency?: string; + tags?: string[]; + images?: string[]; + videoUrl?: string; // Add videoUrl to interface + isActive?: boolean; + workingTime?: string[]; + // Location fields + latitude?: number; + longitude?: number; + address?: string; + city?: string; + state?: string; + country?: string; + postalCode?: string; + serviceRadiusKm?: number; + locationLastUpdated?: Date; +} + +interface ServiceFilters { + providerId?: string; + categoryId?: string; + isActive?: boolean; + skip?: number; + take?: number; +} + +interface LocationSearchOptions { + latitude: number; + longitude: number; + radius: number; + page: number; + limit: number; + categoryId?: string; + minPrice?: number; + maxPrice?: number; +} + +/** + * Create a new service + * @param {ServiceCreateData} serviceData - The service data + * @returns {Promise} Created service object + */ +export const createService = async (serviceData: ServiceCreateData) => { + try { + console.log('=== SERVICE SERVICE DEBUG ==='); + console.log('Service data received in service layer:', JSON.stringify(serviceData, null, 2)); + + const { + providerId, + categoryId, + title, + description, + price, + currency = "USD", + tags = [], + images = [], + isActive = true, + workingTime = [] + } = serviceData; + + // Debug: Extract videoUrl specifically + const videoUrl = serviceData.videoUrl; + console.log('Extracted videoUrl:', videoUrl); + console.log('VideoUrl type:', typeof videoUrl); + + // Validate required fields + if (!providerId) { + throw new Error('Provider ID is required'); + } + if (!categoryId) { + throw new Error('Category ID is required'); + } + if (price === undefined || price === null) { + throw new Error('Price is required'); + } + + // Validate that provider exists + const provider = await prisma.serviceProvider.findUnique({ + where: { id: providerId } + }); + if (!provider) { + throw new Error('Service provider not found'); + } + + // Validate that category exists + const category = await prisma.category.findUnique({ + where: { id: categoryId } + }); + if (!category) { + throw new Error('Category not found'); + } + + // Prepare data for Prisma create + const createData = { + providerId, + categoryId, + title, + description, + price, + currency, + tags, + images, + isActive, + workingTime, + videoUrl, // Make sure videoUrl is included + // Location fields + latitude: serviceData.latitude, + longitude: serviceData.longitude, + address: serviceData.address, + city: serviceData.city, + state: serviceData.state, + country: serviceData.country, + postalCode: serviceData.postalCode, + serviceRadiusKm: serviceData.serviceRadiusKm, + locationLastUpdated: serviceData.locationLastUpdated + }; + + console.log('Data being sent to Prisma create:', JSON.stringify(createData, null, 2)); + console.log('VideoUrl in create data:', createData.videoUrl); + + // Create the service first + const newService = await prisma.service.create({ + data: createData, + include: { + provider: { + include: { + user: { + select: { + firstName: true, + lastName: true, + email: true + } + } + } + }, + category: true + } + }); + + console.log('Service created by Prisma. Checking result...'); + console.log('Service ID:', newService.id); + console.log('Service videoUrl field:', (newService as any).videoUrl); + + // Generate and update embeddings for the newly created service + try { + console.log('Generating embeddings for service:', newService.id); + const embeddings = await embeddingService.generateServiceEmbeddings({ + title: newService.title, + description: newService.description, + tags: newService.tags + }); + + // Update service with embeddings using raw query + await prisma.$executeRaw` + UPDATE "Service" + SET + "titleEmbedding" = ${`[${embeddings.titleEmbedding.join(',')}]`}::vector, + "descriptionEmbedding" = ${`[${embeddings.descriptionEmbedding.join(',')}]`}::vector, + "tagsEmbedding" = ${`[${embeddings.tagsEmbedding.join(',')}]`}::vector, + "combinedEmbedding" = ${`[${embeddings.combinedEmbedding.join(',')}]`}::vector, + "embeddingUpdatedAt" = NOW() + WHERE id = ${newService.id} + `; + + console.log('✅ Embeddings generated and stored for service:', newService.id); + } catch (embeddingError) { + console.warn('âš ī¸ Failed to generate embeddings for service:', newService.id, embeddingError); + // Don't fail the service creation if embedding generation fails + } + + return newService; + } catch (error) { + console.error('=== SERVICE SERVICE ERROR ==='); + console.error('Error in createService:', error); + throw new Error(`Failed to create service: ${error.message}`); + } +}; + +/** + * Get all services with optional filtering + * @param {Object} filters - Optional filters + * @param {string} [filters.providerId] - Filter by provider ID + * @param {string} [filters.categoryId] - Filter by category ID + * @param {boolean} [filters.isActive] - Filter by active status + * @param {number} [filters.skip=0] - Number of records to skip for pagination + * @param {number} [filters.take=10] - Number of records to take for pagination + * @returns {Promise} Array of service objects + */ +export const getServices = async (filters: ServiceFilters = {}) => { + try { + const { + providerId, + categoryId, + isActive = true, // Default to active services only + skip = 0, + take = 20 // Increased default for better UX + } = filters; + + const whereClause: any = {}; + + if (providerId) whereClause.providerId = providerId; + if (categoryId) whereClause.categoryId = categoryId; + if (isActive !== undefined) whereClause.isActive = isActive; + + const services = await prisma.service.findMany({ + where: whereClause, + skip, + take, + include: { + provider: { + include: { + user: { + select: { + firstName: true, + lastName: true, + imageUrl: true + } + } + } + }, + category: { + select: { + id: true, + name: true, + slug: true + } + }, + _count: { + select: { + serviceReviews: true + } + } + }, + orderBy: { + createdAt: 'desc' + } + }); + + return services; + } catch (error) { + throw new Error(`Failed to fetch services: ${error.message}`); + } +}; + +/** + * Get a single service by ID + * @param {string} serviceId - The service ID + * @returns {Promise} Service object or null if not found + */ +export const getServiceById = async (serviceId: string) => { + try { + const service = await prisma.service.findUnique({ + where: { id: serviceId }, + include: { + provider: { + include: { + user: { + select: { + firstName: true, + lastName: true, + email: true, + phone: true, + imageUrl: true + } + } + } + }, + category: { + select: { + id: true, + name: true, + slug: true, + description: true + } + }, + _count: { + select: { + serviceReviews: true, + schedules: true + } + } + } + }); + + console.log('=== GET SERVICE BY ID DEBUG ==='); + console.log('Service found:', service?.id); + console.log('Service videoUrl:', (service as any)?.videoUrl); + + return service; + } catch (error) { + throw new Error(`Failed to fetch service: ${error.message}`); + } +}; + +/** + * Update a service + * @param {string} serviceId - The service ID + * @param {Object} updateData - Data to update + * @returns {Promise} Updated service object + */ +export const updateService = async (serviceId: string, updateData: Partial) => { + try { + const service = await prisma.service.findUnique({ + where: { id: serviceId } + }); + + if (!service) { + throw new Error('Service not found'); + } + + // Check if content that affects embeddings has changed + const contentChanged = ( + (updateData.title !== undefined && updateData.title !== service.title) || + (updateData.description !== undefined && updateData.description !== service.description) || + (updateData.tags !== undefined && JSON.stringify(updateData.tags) !== JSON.stringify(service.tags)) + ); + + const updatedService = await prisma.service.update({ + where: { id: serviceId }, + data: updateData, + include: { + provider: { + include: { + user: { + select: { + firstName: true, + lastName: true, + email: true + } + } + } + }, + category: true + } + }); + + // Regenerate embeddings if content changed + if (contentChanged) { + try { + console.log('Content changed, regenerating embeddings for service:', serviceId); + const embeddings = await embeddingService.generateServiceEmbeddings({ + title: updatedService.title, + description: updatedService.description, + tags: updatedService.tags + }); + + // Update service with new embeddings using raw query + await prisma.$executeRaw` + UPDATE "Service" + SET + "titleEmbedding" = ${`[${embeddings.titleEmbedding.join(',')}]`}::vector, + "descriptionEmbedding" = ${`[${embeddings.descriptionEmbedding.join(',')}]`}::vector, + "tagsEmbedding" = ${`[${embeddings.tagsEmbedding.join(',')}]`}::vector, + "combinedEmbedding" = ${`[${embeddings.combinedEmbedding.join(',')}]`}::vector, + "embeddingUpdatedAt" = NOW() + WHERE id = ${serviceId} + `; + + console.log('✅ Embeddings regenerated for updated service:', serviceId); + } catch (embeddingError) { + console.warn('âš ī¸ Failed to regenerate embeddings for service:', serviceId, embeddingError); + // Don't fail the service update if embedding generation fails + } + } + + return updatedService; + } catch (error) { + throw new Error(`Failed to update service: ${error.message}`); + } +}; + +/** + * Delete a service + * @param {string} serviceId - The service ID + * @returns {Promise} Deleted service object + */ +export const deleteService = async (serviceId: string) => { + try { + const service = await prisma.service.findUnique({ + where: { id: serviceId } + }); + + if (!service) { + throw new Error('Service not found'); + } + + const deletedService = await prisma.service.delete({ + where: { id: serviceId } + }); + + return deletedService; + } catch (error) { + throw new Error(`Failed to delete service: ${error.message}`); + } +}; + +/** + * Get a service by conversation ID + * @param {string} conversationId - The conversation ID + * @returns {Promise} Service object or null if not found + */ +export const getServiceByConversationId = async (conversationId: string) => { + try { + const conversation = await prisma.conversation.findUnique({ + where: { id: conversationId }, + include: { + service: { + include: { + provider: { + include: { + user: { + select: { + firstName: true, + lastName: true, + email: true, + phone: true, + imageUrl: true + } + } + } + }, + category: { + select: { + id: true, + name: true, + slug: true, + description: true + } + }, + _count: { + select: { + serviceReviews: true, + schedules: true + } + } + } + } + } + }); + + if (!conversation || !conversation.service) { + return null; + } + + return conversation.service; + } catch (error) { + throw new Error(`Failed to fetch service by conversation ID: ${error.message}`); + } +}; + +/** + * Search services by location using PostGIS spatial queries + * @param {LocationSearchOptions} options - Search options + * @returns {Promise} Services and pagination info + */ +export const searchServicesByLocation = async (options: LocationSearchOptions) => { + try { + const { + latitude, + longitude, + radius, + page, + limit, + categoryId, + minPrice, + maxPrice + } = options; + + const offset = (page - 1) * limit; + + // Build WHERE clause for additional filters + let whereConditions = ['s."isActive" = true']; + const queryParams: any[] = [longitude, latitude, radius * 1000, limit, offset]; // radius in meters + let paramIndex = 6; + + if (categoryId) { + whereConditions.push(`s."categoryId" = $${paramIndex}`); + queryParams.push(categoryId); + paramIndex++; + } + + if (minPrice !== undefined) { + whereConditions.push(`s.price >= $${paramIndex}`); + queryParams.push(minPrice); + paramIndex++; + } + + if (maxPrice !== undefined) { + whereConditions.push(`s.price <= $${paramIndex}`); + queryParams.push(maxPrice); + paramIndex++; + } + + const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(' AND ')}` : ''; + + // Main query to get services within radius + const servicesQuery = ` + SELECT + s.*, + ST_Distance( + ST_SetSRID(ST_MakePoint(s.longitude, s.latitude), 4326)::geography, + ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography + ) / 1000 as distance_km, + sp.id as provider_id, + sp."averageRating" as provider_average_rating, + sp."totalReviews" as provider_total_reviews, + u."firstName" as provider_first_name, + u."lastName" as provider_last_name, + u."imageUrl" as provider_image_url, + c.name as category_name, + c.slug as category_slug + FROM "Service" s + INNER JOIN "ServiceProvider" sp ON s."providerId" = sp.id + INNER JOIN "User" u ON sp."userId" = u.id + INNER JOIN "Category" c ON s."categoryId" = c.id + ${whereClause} + AND s.latitude IS NOT NULL + AND s.longitude IS NOT NULL + AND ST_DWithin( + ST_SetSRID(ST_MakePoint(s.longitude, s.latitude), 4326)::geography, + ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography, + $3 + ) + ORDER BY distance_km ASC + LIMIT $4 OFFSET $5 + `; + + // Count query for pagination + const countQuery = ` + SELECT COUNT(*) + FROM "Service" s + ${whereClause} + AND s.latitude IS NOT NULL + AND s.longitude IS NOT NULL + AND ST_DWithin( + ST_SetSRID(ST_MakePoint(s.longitude, s.latitude), 4326)::geography, + ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography, + $3 + ) + `; + + // Execute queries + const [servicesResult, countResult] = await Promise.all([ + prisma.$queryRawUnsafe(servicesQuery, ...queryParams), + prisma.$queryRawUnsafe(countQuery, longitude, latitude, radius * 1000, ...queryParams.slice(5, paramIndex - 1)) + ]); + + const services = servicesResult as any[]; + const total = parseInt((countResult as any[])[0].count); + + // Format the results + const formattedServices = services.map(service => ({ + id: service.id, + title: service.title, + description: service.description, + price: parseFloat(service.price), + currency: service.currency, + tags: service.tags, + images: service.images, + videoUrl: service.videoUrl, + isActive: service.isActive, + workingTime: service.workingTime, + latitude: parseFloat(service.latitude), + longitude: parseFloat(service.longitude), + address: service.address, + city: service.city, + state: service.state, + country: service.country, + postalCode: service.postalCode, + serviceRadiusKm: service.serviceRadiusKm ? parseFloat(service.serviceRadiusKm) : null, + distance_km: parseFloat(service.distance_km), + createdAt: service.createdAt, + updatedAt: service.updatedAt, + provider: { + id: service.provider_id, + averageRating: service.provider_average_rating ? parseFloat(service.provider_average_rating) : null, + totalReviews: service.provider_total_reviews, + user: { + firstName: service.provider_first_name, + lastName: service.provider_last_name, + imageUrl: service.provider_image_url + } + }, + category: { + name: service.category_name, + slug: service.category_slug + } + })); + + return { + services: formattedServices, + total + }; + } catch (error) { + console.error('Location search error:', error); + throw new Error(`Failed to search services by location: ${error.message}`); + } +}; + diff --git a/src/services/task.service.ts b/src/services/task.service.ts new file mode 100755 index 0000000..ab407dc --- /dev/null +++ b/src/services/task.service.ts @@ -0,0 +1,505 @@ +import { prisma } from '../utils/database'; +import { TaskStatus, TaskPriority, ActivityType, EntityType } from '@prisma/client'; +import { createActivity } from './activity.service'; + +// Global socket service instance (will be set from the main app) +let socketService: any = null; + +export const setTaskSocketService = (service: any) => { + socketService = service; +}; + +export interface CreateTaskData { + title: string; + description?: string; + priority?: TaskPriority; + dueDate?: Date; + teamId?: string; + assigneeIds?: string[]; +} + +export interface UpdateTaskData { + title?: string; + description?: string; + status?: TaskStatus; + priority?: TaskPriority; + dueDate?: Date; +} + +export interface TaskFilters { + status?: TaskStatus; + priority?: TaskPriority; + assigneeId?: string; + teamId?: string; + createdById?: string; + dueDateBefore?: Date; + dueDateAfter?: Date; + search?: string; +} + +export const createTask = async (userId: string, taskData: CreateTaskData) => { + const { assigneeIds, ...taskDetails } = taskData; + + const task = await prisma.task.create({ + data: { + ...taskDetails, + createdById: userId, + assignments: assigneeIds ? { + create: assigneeIds.map(assigneeId => ({ + userId: assigneeId, + assignedById: userId + })) + } : undefined + }, + include: { + createdBy: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + }, + team: { + select: { + id: true, + name: true + } + }, + assignments: { + include: { + user: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + } + } + }, + _count: { + select: { + comments: true, + dependencies: true, + dependentOn: true + } + } + } + }); + + // Create activity + await createActivity({ + type: ActivityType.TASK_CREATED, + description: `Task "${task.title}" was created`, + entityType: EntityType.TASK, + entityId: task.id, + userId, + teamId: task.teamId || undefined, + metadata: { + taskTitle: task.title, + priority: task.priority + } + }); + + return task; +}; + +export const getTasks = async (userId: string, filters: TaskFilters = {}, page = 1, limit = 10) => { + const skip = (page - 1) * limit; + + // Build filter conditions + const filterConditions: any = {}; + + // Apply filters + if (filters.status) filterConditions.status = filters.status; + if (filters.priority) filterConditions.priority = filters.priority; + if (filters.teamId) filterConditions.teamId = filters.teamId; + if (filters.createdById) filterConditions.createdById = filters.createdById; + if (filters.dueDateBefore || filters.dueDateAfter) { + filterConditions.dueDate = {}; + if (filters.dueDateBefore) filterConditions.dueDate.lte = filters.dueDateBefore; + if (filters.dueDateAfter) filterConditions.dueDate.gte = filters.dueDateAfter; + } + + // Search filter + if (filters.search) { + filterConditions.OR = [ + { title: { contains: filters.search, mode: 'insensitive' } }, + { description: { contains: filters.search, mode: 'insensitive' } } + ]; + } + + // Filter by assignment + if (filters.assigneeId) { + filterConditions.assignments = { + some: { + userId: filters.assigneeId + } + }; + } + + // Base security filter: user can only see tasks they're involved with + const userInvolvementFilter = { + OR: [ + { createdById: userId }, + { assignments: { some: { userId } } }, + { team: { members: { some: { userId } } } } + ] + }; + + // Combine all conditions + const where = { + AND: [ + filterConditions, + userInvolvementFilter + ] + }; + + const [tasks, total] = await Promise.all([ + prisma.task.findMany({ + where, + skip, + take: limit, + orderBy: [ + { priority: 'desc' }, + { dueDate: 'asc' }, + { createdAt: 'desc' } + ], + include: { + createdBy: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + }, + team: { + select: { + id: true, + name: true + } + }, + assignments: { + include: { + user: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + } + } + }, + _count: { + select: { + comments: true, + dependencies: true, + dependentOn: true + } + } + } + }), + prisma.task.count({ where }) + ]); + + return { + tasks, + pagination: { + current: page, + total: Math.ceil(total / limit), + count: tasks.length, + totalCount: total + } + }; +}; + +export const getTaskById = async (taskId: string, userId: string) => { + const task = await prisma.task.findFirst({ + where: { + id: taskId, + OR: [ + { createdById: userId }, + { assignments: { some: { userId } } }, + { team: { members: { some: { userId } } } } + ] + }, + include: { + createdBy: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + }, + team: { + select: { + id: true, + name: true, + description: true + } + }, + assignments: { + include: { + user: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + } + } + }, + comments: { + include: { + user: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + } + }, + orderBy: { createdAt: 'desc' } + }, + dependencies: { + include: { + dependsOnTask: { + select: { + id: true, + title: true, + status: true + } + } + } + }, + dependentOn: { + include: { + task: { + select: { + id: true, + title: true, + status: true + } + } + } + } + } + }); + + if (!task) { + throw new Error('Task not found or access denied'); + } + + return task; +}; + +export const updateTask = async (taskId: string, userId: string, updateData: UpdateTaskData) => { + // Check if task exists + const existingTask = await prisma.task.findUnique({ + where: { id: taskId } + }); + + if (!existingTask) { + throw new Error('Task not found'); + } + + // If trying to mark as DONE, check dependencies + if (updateData.status === TaskStatus.DONE && existingTask.status !== TaskStatus.DONE) { + const dependencies = await prisma.taskDependency.findMany({ + where: { taskId }, + include: { + dependsOnTask: { + select: { + id: true, + title: true, + status: true + } + } + } + }); + + const incompleteDependencies = dependencies.filter( + d => d.dependsOnTask.status !== TaskStatus.DONE + ); + + if (incompleteDependencies.length > 0) { + const blockedByTitles = incompleteDependencies.map(d => d.dependsOnTask.title).join(', '); + throw new Error(`Cannot complete task. Blocked by incomplete dependencies: ${blockedByTitles}`); + } + } + + const task = await prisma.task.update({ + where: { id: taskId }, + data: { + ...updateData, + completedAt: updateData.status === TaskStatus.DONE ? new Date() : null + }, + include: { + createdBy: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + }, + team: { + select: { + id: true, + name: true + } + }, + assignments: { + include: { + user: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + } + } + } + } + }); + + // Create activity + await createActivity({ + type: ActivityType.TASK_UPDATED, + description: `Task "${task.title}" was updated`, + entityType: EntityType.TASK, + entityId: task.id, + userId, + teamId: task.teamId || undefined, + metadata: { + taskTitle: task.title, + changes: updateData + } + }); + + // Broadcast task update in real-time + if (socketService) { + socketService.broadcastTaskUpdate(task); + } + + return task; +}; + +export const deleteTask = async (taskId: string, userId: string) => { + // First check if user has permission to delete this task + const existingTask = await prisma.task.findFirst({ + where: { + id: taskId, + OR: [ + { createdById: userId }, + { team: { members: { some: { userId, role: { in: ['OWNER', 'ADMIN'] } } } } } + ] + } + }); + + if (!existingTask) { + throw new Error('Task not found or insufficient permissions'); + } + + await prisma.task.delete({ + where: { id: taskId } + }); + + return { message: 'Task deleted successfully' }; +}; + +export const assignUsersToTask = async (taskId: string, userIds: string[], assignedById: string) => { + // Check if task exists and user has permission + const task = await prisma.task.findFirst({ + where: { + id: taskId, + OR: [ + { createdById: assignedById }, + { team: { members: { some: { userId: assignedById, role: { in: ['OWNER', 'ADMIN'] } } } } } + ] + } + }); + + if (!task) { + throw new Error('Task not found or insufficient permissions'); + } + + // Remove existing assignments + await prisma.taskAssignment.deleteMany({ + where: { taskId } + }); + + // Create new assignments + if (userIds.length > 0) { + await prisma.taskAssignment.createMany({ + data: userIds.map(userId => ({ + taskId, + userId, + assignedById + })) + }); + + // Create activity + await createActivity({ + type: ActivityType.TASK_ASSIGNED, + description: `Task "${task.title}" was assigned to ${userIds.length} user(s)`, + entityType: EntityType.TASK, + entityId: taskId, + userId: assignedById, + teamId: task.teamId || undefined, + metadata: { + taskTitle: task.title, + assigneeIds: userIds + } + }); + } + + return { message: 'Task assignments updated successfully' }; +}; + +export const getTaskStatistics = async (userId: string, teamId?: string) => { + const where: any = {}; + + if (teamId) { + where.teamId = teamId; + } else { + where.OR = [ + { createdById: userId }, + { assignments: { some: { userId } } }, + { team: { members: { some: { userId } } } } + ]; + } + + const [total, todo, inProgress, inReview, done, overdue] = await Promise.all([ + prisma.task.count({ where }), + prisma.task.count({ where: { ...where, status: TaskStatus.TODO } }), + prisma.task.count({ where: { ...where, status: TaskStatus.IN_PROGRESS } }), + prisma.task.count({ where: { ...where, status: TaskStatus.IN_REVIEW } }), + prisma.task.count({ where: { ...where, status: TaskStatus.DONE } }), + prisma.task.count({ + where: { + ...where, + dueDate: { lt: new Date() }, + status: { notIn: [TaskStatus.DONE, TaskStatus.CANCELLED] } + } + }) + ]); + + return { + total, + byStatus: { + todo, + inProgress, + inReview, + done + }, + overdue + }; +}; \ No newline at end of file diff --git a/src/services/team.service.ts b/src/services/team.service.ts new file mode 100755 index 0000000..2448078 --- /dev/null +++ b/src/services/team.service.ts @@ -0,0 +1,503 @@ +import { prisma } from '../utils/database'; +import { TeamRole, ActivityType, EntityType } from '@prisma/client'; +import { createActivity } from './activity.service'; + +export interface CreateTeamData { + name: string; + description?: string; +} + +export interface UpdateTeamData { + name?: string; + description?: string; +} + +export interface TeamMemberData { + userId: string; + role?: TeamRole; +} + +export const createTeam = async (creatorId: string, teamData: CreateTeamData) => { + console.log('🔄 Team service: Creating team'); + console.log('👤 Creator ID:', creatorId); + console.log('📋 Team data:', teamData); + + try { + console.log('💾 Creating team in database...'); + const team = await prisma.team.create({ + data: { + ...teamData, + members: { + create: { + userId: creatorId, + role: TeamRole.OWNER + } + } + }, + include: { + members: { + include: { + user: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + } + } + }, + _count: { + select: { + tasks: true + } + } + } + }); + + console.log('✅ Team created in database:', team); + console.log('📈 Creating activity log...'); + + // Create activity + await createActivity({ + type: ActivityType.TEAM_CREATED, + description: `Team "${team.name}" was created`, + entityType: EntityType.TEAM, + entityId: team.id, + userId: creatorId, + teamId: team.id, + metadata: { + teamName: team.name + } + }); + + console.log('✅ Activity log created successfully'); + return team; + } catch (error) { + console.error('❌ Error in team service:', error); + throw error; + } +}; + +export const getTeams = async (userId: string) => { + return await prisma.team.findMany({ + where: { + members: { + some: { + userId + } + } + }, + include: { + members: { + include: { + user: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + } + } + }, + _count: { + select: { + tasks: true + } + } + }, + orderBy: { + createdAt: 'desc' + } + }); +}; + +export const getTeamById = async (teamId: string, userId: string) => { + const team = await prisma.team.findFirst({ + where: { + id: teamId, + members: { + some: { + userId + } + } + }, + include: { + members: { + include: { + user: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + } + }, + orderBy: { + joinedAt: 'asc' + } + }, + tasks: { + include: { + createdBy: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + }, + assignments: { + include: { + user: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + } + } + }, + _count: { + select: { + comments: true + } + } + }, + orderBy: { + createdAt: 'desc' + } + }, + activities: { + include: { + user: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + } + }, + orderBy: { + createdAt: 'desc' + }, + take: 20 + } + } + }); + + if (!team) { + throw new Error('Team not found or access denied'); + } + + return team; +}; + +export const updateTeam = async (teamId: string, userId: string, updateData: UpdateTeamData) => { + // Check if user has permission to update team + const membership = await prisma.teamMember.findFirst({ + where: { + teamId, + userId, + role: { in: [TeamRole.OWNER, TeamRole.ADMIN] } + } + }); + + if (!membership) { + throw new Error('Team not found or insufficient permissions'); + } + + const team = await prisma.team.update({ + where: { id: teamId }, + data: updateData, + include: { + members: { + include: { + user: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + } + } + }, + _count: { + select: { + tasks: true + } + } + } + }); + + return team; +}; + +export const deleteTeam = async (teamId: string, userId: string) => { + // Check if user is the owner + const membership = await prisma.teamMember.findFirst({ + where: { + teamId, + userId, + role: TeamRole.OWNER + } + }); + + if (!membership) { + throw new Error('Team not found or insufficient permissions'); + } + + await prisma.team.delete({ + where: { id: teamId } + }); + + return { message: 'Team deleted successfully' }; +}; + +export const addTeamMember = async (teamId: string, adminId: string, memberData: TeamMemberData) => { + console.log('DEBUG - addTeamMember called with:', { teamId, adminId, memberData }); + + try { + // Check if user has permission to add members + console.log('Checking admin permissions...'); + const adminMembership = await prisma.teamMember.findFirst({ + where: { + teamId, + userId: adminId, + role: { in: [TeamRole.OWNER, TeamRole.ADMIN] } + } + }); + + if (!adminMembership) { + console.log('ERROR - Admin membership not found or insufficient permissions'); + throw new Error('Team not found or insufficient permissions'); + } + console.log('Admin permissions OK:', adminMembership); + + // Check if user is already a member + console.log('Checking for existing membership...'); + const existingMembership = await prisma.teamMember.findFirst({ + where: { + teamId, + userId: memberData.userId + } + }); + + if (existingMembership) { + console.log('ERROR - User is already a member'); + throw new Error('User is already a member of this team'); + } + console.log('No existing membership found'); + + // Check if user exists + console.log('Checking if user exists...'); + const user = await prisma.user.findUnique({ + where: { id: memberData.userId }, + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + }); + + if (!user) { + console.log('ERROR - User not found'); + throw new Error('User not found'); + } + console.log('User found:', user); + + console.log('Creating team membership...'); + const membership = await prisma.teamMember.create({ + data: { + teamId, + userId: memberData.userId, + role: memberData.role || TeamRole.MEMBER + }, + include: { + user: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + }, + team: { + select: { + id: true, + name: true + } + } + } + }); + console.log('Membership created successfully:', membership); + + // Create activity - temporarily disabled due to foreign key constraint issue + console.log('Skipping activity creation to avoid foreign key constraint issue'); + // await createActivity({ + // type: ActivityType.TEAM_JOINED, + // description: `${user.firstName || user.email} joined the team`, + // entityType: EntityType.TEAM, + // entityId: teamId, // For team activities, use teamId as entityId + // userId: memberData.userId, + // teamId, + // metadata: { + // memberName: user.firstName || user.email, + // role: membership.role + // } + // }); + console.log('Activity created successfully'); + + return membership; + } catch (error) { + console.log('ERROR in addTeamMember:', error); + throw error; + } +}; + +export const removeTeamMember = async (teamId: string, adminId: string, memberUserId: string) => { + // Check if user has permission to remove members + const adminMembership = await prisma.teamMember.findFirst({ + where: { + teamId, + userId: adminId, + role: { in: [TeamRole.OWNER, TeamRole.ADMIN] } + } + }); + + if (!adminMembership) { + throw new Error('Team not found or insufficient permissions'); + } + + // Can't remove the owner + const memberMembership = await prisma.teamMember.findFirst({ + where: { + teamId, + userId: memberUserId + } + }); + + if (!memberMembership) { + throw new Error('Member not found'); + } + + if (memberMembership.role === TeamRole.OWNER) { + throw new Error('Cannot remove team owner'); + } + + // Only owner can remove admins + if (memberMembership.role === TeamRole.ADMIN && adminMembership.role !== TeamRole.OWNER) { + throw new Error('Only team owner can remove administrators'); + } + + await prisma.teamMember.delete({ + where: { + id: memberMembership.id + } + }); + + // Create activity - temporarily disabled due to foreign key constraint issue + // await createActivity({ + // type: ActivityType.TEAM_LEFT, + // description: `A member left the team`, + // entityType: EntityType.TEAM, + // entityId: teamId, + // userId: adminId, + // teamId, + // metadata: { + // removedUserId: memberUserId + // } + // }); + + return { message: 'Member removed successfully' }; +}; + +export const updateTeamMemberRole = async (teamId: string, adminId: string, memberUserId: string, newRole: TeamRole) => { + // Check if user has permission to update roles + const adminMembership = await prisma.teamMember.findFirst({ + where: { + teamId, + userId: adminId, + role: TeamRole.OWNER // Only owner can change roles + } + }); + + if (!adminMembership) { + throw new Error('Team not found or insufficient permissions'); + } + + // Can't change owner role or change role to owner + const memberMembership = await prisma.teamMember.findFirst({ + where: { + teamId, + userId: memberUserId + } + }); + + if (!memberMembership) { + throw new Error('Member not found'); + } + + if (memberMembership.role === TeamRole.OWNER || newRole === TeamRole.OWNER) { + throw new Error('Cannot change ownership through this endpoint'); + } + + const updatedMembership = await prisma.teamMember.update({ + where: { id: memberMembership.id }, + data: { role: newRole }, + include: { + user: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + } + } + }); + + return updatedMembership; +}; + +export const leaveTeam = async (teamId: string, userId: string) => { + const membership = await prisma.teamMember.findFirst({ + where: { + teamId, + userId + } + }); + + if (!membership) { + throw new Error('You are not a member of this team'); + } + + if (membership.role === TeamRole.OWNER) { + throw new Error('Team owner cannot leave the team. Transfer ownership or delete the team instead.'); + } + + await prisma.teamMember.delete({ + where: { id: membership.id } + }); + + // Create activity + await createActivity({ + type: ActivityType.TEAM_LEFT, + description: `A member left the team`, + entityType: EntityType.TEAM, + entityId: teamId, + userId, + teamId, + metadata: { + leftUserId: userId + } + }); + + return { message: 'Left team successfully' }; +}; \ No newline at end of file diff --git a/src/services/user.service.js b/src/services/user.service.js deleted file mode 100644 index 38e47cd..0000000 --- a/src/services/user.service.js +++ /dev/null @@ -1,131 +0,0 @@ - -import { PrismaClient } from '@prisma/client'; -import jwt from 'jsonwebtoken'; -import { comparePassword, hashPassword } from '../utils/hash.js'; - -const prisma = new PrismaClient(); - -export const register = async ({ email, firstName, lastName, password, imageUrl, location, address, phone, socialmedia }) => { - const existingUser = await prisma.user.findUnique({ where: { email } }); - if (existingUser) { - const err = new Error('Email already exists. Please use a different email address.'); - err.name = 'BadRequestError'; - err.status = 400; - throw err; - } - const hashedPassword = await hashPassword(password); - return await prisma.user.create({ - data: { - email, - firstName, - lastName, - password: hashedPassword, - imageUrl, - location, - address, - phone, - socialmedia, - }, - }); -}; - -export const login = async ({ email, password }) => { - const user = await prisma.user.findUnique({ where: { email } }); - if (!user) throw new Error('User not found'); - - const isMatch = await comparePassword(password, user.password); - if (!isMatch) throw new Error('Invalid credentials'); - - const token = jwt.sign({ id: user.id, email: user.email }, process.env.JWT_SECRET, { expiresIn: '1h' }); - return { token, user }; -}; - -export const getProfile = async (userId) => { - const user = await prisma.user.findUnique({ - where: { id: userId }, - select: { - id: true, - email: true, - role: true, - firstName: true, - lastName: true, - imageUrl: true, - location: true, - address: true, - phone: true, - socialmedia: true, - createdAt: true, - isEmailVerified: true, - serviceProvider: { - select: { - id: true, - bio: true, - skills: true, - qualifications: true, - logoUrl: true, - averageRating: true, - totalReviews: true, - services: { - select: { - id: true, - title: true, - description: true, - price: true, - currency: true, - images: true, - isActive: true - } - }, - reviews: { - select: { - id: true, - rating: true, - comment: true, - createdAt: true, - reviewer: { - select: { - firstName: true, - lastName: true, - imageUrl: true - } - } - }, - orderBy: { - createdAt: 'desc' - }, - take: 10 - } - } - } - }, - }); - if (!user) throw new Error('User not found'); - return user; -} - -export const updateProfile = async (userId, data) => { - const updatedData = {}; - if (data.firstName) updatedData.firstName = data.firstName; - if (data.lastName) updatedData.lastName = data.lastName; - if (data.imageUrl) updatedData.imageUrl = data.imageUrl; - if (data.location) updatedData.location = data.location; - if (data.address) updatedData.address = data.address; - if (data.phone) updatedData.phone = data.phone; - if (data.socialmedia) updatedData.socialmedia = data.socialmedia; - - return await prisma.user.update({ - where: { id: userId }, - data: updatedData, - }); -} - -export const deleteProfile = async (userId) => { - await prisma.user.delete({ - where: { id: userId }, - }); -} - -export const checkEmailExists = async (email) => { - const user = await prisma.user.findUnique({ where: { email } }); - return !!user; -} \ No newline at end of file diff --git a/src/services/user.service.ts b/src/services/user.service.ts new file mode 100755 index 0000000..4ff2b16 --- /dev/null +++ b/src/services/user.service.ts @@ -0,0 +1,212 @@ +import { prisma } from '../utils/database'; +import jwt from 'jsonwebtoken'; +import { comparePassword, hashPassword } from '../utils/hash'; + +// Type definitions +interface UserRegistrationData { + email: string; + firstName?: string; + lastName?: string; + password: string; + address?: string; + phone?: string; +} + +interface UserUpdateData { + firstName?: string; + lastName?: string; + phone?: string; + address?: string; + bio?: string; +} + +interface LoginData { + email: string; + password: string; +} + +interface ErrorWithStatus extends Error { + status?: number; +} + +// Register a new user +export const register = async ({ email, firstName, lastName, password, address, phone }: UserRegistrationData) => { + const existingUser = await prisma.user.findUnique({ where: { email } }); + if (existingUser) { + const err = new Error('Email already exists. Please use a different email address.') as ErrorWithStatus; + err.name = 'BadRequestError'; + err.status = 400; + throw err; + } + + const hashedPassword = await hashPassword(password); + const user = await prisma.user.create({ + data: { + email, + firstName, + lastName, + password: hashedPassword, + address, + phone, + }, + }); + + // Return user data without password + const { password: _, ...userWithoutPassword } = user; + return userWithoutPassword; +}; + +// Login user +export const login = async ({ email, password }: LoginData) => { + const user = await prisma.user.findUnique({ where: { email } }); + if (!user) throw new Error('User not found'); + + const isMatch = await comparePassword(password, user.password); + if (!isMatch) throw new Error('Invalid credentials'); + + if (!process.env.JWT_SECRET) { + throw new Error('JWT_SECRET is not defined'); + } + + const token = jwt.sign({ id: user.id, email: user.email }, process.env.JWT_SECRET, { expiresIn: '1h' }); + + // Update last login time + await prisma.user.update({ + where: { id: user.id }, + data: { lastLoginAt: new Date() } + }); + + // Return user data without password + const { password: _, ...userWithoutPassword } = user; + return { token, user: userWithoutPassword }; +}; + +// Get user profile +export const getProfile = async (userId: string) => { + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { + id: true, + email: true, + firstName: true, + lastName: true, + phone: true, + address: true, + bio: true, + createdAt: true, + updatedAt: true, + lastLoginAt: true, + }, + }); + + if (!user) { + throw new Error('User not found'); + } + + return user; +}; + +// Update user profile +export const updateProfile = async (userId: string, data: UserUpdateData) => { + const updatedUser = await prisma.user.update({ + where: { id: userId }, + data: { + firstName: data.firstName, + lastName: data.lastName, + phone: data.phone, + address: data.address, + bio: data.bio, + }, + select: { + id: true, + email: true, + firstName: true, + lastName: true, + phone: true, + address: true, + bio: true, + createdAt: true, + updatedAt: true, + lastLoginAt: true, + }, + }); + + return updatedUser; +}; + +// Delete user profile +export const deleteProfile = async (userId: string) => { + await prisma.user.delete({ + where: { id: userId }, + }); +}; + +// Check if email exists +export const checkEmailExists = async (email: string): Promise => { + const user = await prisma.user.findUnique({ + where: { email }, + select: { id: true }, + }); + return !!user; +}; + +// Get user by ID +export const getUserById = async (userId: string) => { + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { + id: true, + email: true, + firstName: true, + lastName: true, + phone: true, + address: true, + bio: true, + createdAt: true, + updatedAt: true, + lastLoginAt: true, + }, + }); + + if (!user) { + throw new Error('User not found'); + } + + return user; +}; + +// Search users by email for team member invitation +export const searchUsersByEmail = async (emailQuery: string) => { + console.log('🔍 Searching users with email query:', emailQuery); + + try { + // Validate input + if (!emailQuery || typeof emailQuery !== 'string') { + console.log('âš ī¸ Invalid email query, returning empty array'); + return []; + } + + const users = await prisma.user.findMany({ + where: { + email: { + contains: emailQuery, + mode: 'insensitive' + } + }, + select: { + id: true, + email: true, + firstName: true, + lastName: true, + }, + take: 10 // Limit results to 10 users + }); + + console.log('✅ Search results:', users.length, 'users found'); + return users || []; // Ensure we always return an array + } catch (error) { + console.error('❌ Error in searchUsersByEmail:', error); + // Return empty array instead of throwing error + return []; + } +}; \ No newline at end of file diff --git a/src/types/env.d.ts b/src/types/env.d.ts new file mode 100755 index 0000000..5d8a796 --- /dev/null +++ b/src/types/env.d.ts @@ -0,0 +1,8 @@ +declare namespace NodeJS { + interface ProcessEnv { + JWT_SECRET: string; + DATABASE_URL: string; + PORT?: string; + NODE_ENV?: string; + } +} \ No newline at end of file diff --git a/src/utils/database.ts b/src/utils/database.ts new file mode 100755 index 0000000..037e65a --- /dev/null +++ b/src/utils/database.ts @@ -0,0 +1,20 @@ +import { PrismaClient } from '@prisma/client'; + +export const prisma = new PrismaClient({ + log: ['query', 'info', 'warn', 'error'], +}); + +// Graceful shutdown +process.on('beforeExit', async () => { + await prisma.$disconnect(); +}); + +process.on('SIGINT', async () => { + await prisma.$disconnect(); + process.exit(0); +}); + +process.on('SIGTERM', async () => { + await prisma.$disconnect(); + process.exit(0); +}); \ No newline at end of file diff --git a/src/utils/hash.js b/src/utils/hash.js deleted file mode 100644 index 96d80b5..0000000 --- a/src/utils/hash.js +++ /dev/null @@ -1,9 +0,0 @@ -import { hash as _hash, compare } from 'bcrypt'; - -export async function hashPassword(plainText) { - return await _hash(plainText, 10); -} - -export async function comparePassword(plainText, hash) { - return await compare(plainText, hash); -} diff --git a/src/utils/hash.ts b/src/utils/hash.ts new file mode 100755 index 0000000..bcc4a00 --- /dev/null +++ b/src/utils/hash.ts @@ -0,0 +1,10 @@ +import bcrypt from 'bcryptjs'; + +export const hashPassword = async (password: string): Promise => { + const saltRounds = 12; + return await bcrypt.hash(password, saltRounds); +}; + +export const comparePassword = async (password: string, hashedPassword: string): Promise => { + return await bcrypt.compare(password, hashedPassword); +}; \ No newline at end of file diff --git a/src/utils/socket.ts b/src/utils/socket.ts new file mode 100644 index 0000000..580da3c --- /dev/null +++ b/src/utils/socket.ts @@ -0,0 +1,282 @@ +import { Server as HttpServer } from 'http'; +import { Server as SocketIOServer, Socket } from 'socket.io'; +import jwt from 'jsonwebtoken'; +import { prisma } from './database'; + +interface AuthenticatedSocket extends Socket { + userId?: string; +} + +class SocketService { + private io: SocketIOServer; + private userSockets: Map = new Map(); // userId -> socketIds[] + + constructor(server: HttpServer) { + this.io = new SocketIOServer(server, { + cors: { + origin: ['http://localhost:5173', 'http://localhost:3000'], + methods: ['GET', 'POST'], + credentials: true + } + }); + + this.initializeSocketHandlers(); + } + + private initializeSocketHandlers() { + this.io.use(async (socket: AuthenticatedSocket, next) => { + try { + const token = socket.handshake.auth.token; + if (!token) { + throw new Error('No token provided'); + } + + const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { userId: string }; + socket.userId = decoded.userId; + next(); + } catch (error) { + next(new Error('Authentication failed')); + } + }); + + this.io.on('connection', (socket: AuthenticatedSocket) => { + console.log(`User ${socket.userId} connected`); + + // Add socket to user's socket list + if (socket.userId) { + const userSockets = this.userSockets.get(socket.userId) || []; + userSockets.push(socket.id); + this.userSockets.set(socket.userId, userSockets); + + // Join user to their personal room + socket.join(`user:${socket.userId}`); + + // Join user to their team rooms + this.joinUserTeams(socket); + } + + socket.on('disconnect', () => { + console.log(`User ${socket.userId} disconnected`); + + // Remove socket from user's socket list + if (socket.userId) { + const userSockets = this.userSockets.get(socket.userId) || []; + const updatedSockets = userSockets.filter(id => id !== socket.id); + + if (updatedSockets.length === 0) { + this.userSockets.delete(socket.userId); + } else { + this.userSockets.set(socket.userId, updatedSockets); + } + } + }); + + // Handle joining specific rooms + socket.on('join-team', (teamId: string) => { + if (socket.userId) { + this.joinTeamRoom(socket, teamId); + } + }); + + socket.on('leave-team', (teamId: string) => { + socket.leave(`team:${teamId}`); + }); + + // Handle activity requests + socket.on('request-activities', async (filters: any) => { + if (socket.userId) { + try { + const activities = await this.getActivitiesForUser(socket.userId, filters); + socket.emit('activities', activities); + } catch (error) { + socket.emit('error', { message: 'Failed to fetch activities' }); + } + } + }); + }); + } + + private async joinUserTeams(socket: AuthenticatedSocket) { + if (!socket.userId) return; + + try { + const teams = await prisma.teamMember.findMany({ + where: { userId: socket.userId }, + select: { teamId: true } + }); + + teams.forEach(team => { + socket.join(`team:${team.teamId}`); + }); + } catch (error) { + console.error('Error joining user teams:', error); + } + } + + private async joinTeamRoom(socket: AuthenticatedSocket, teamId: string) { + if (!socket.userId) return; + + try { + // Verify user is member of the team + const membership = await prisma.teamMember.findFirst({ + where: { + userId: socket.userId, + teamId + } + }); + + if (membership) { + socket.join(`team:${teamId}`); + socket.emit('joined-team', teamId); + } else { + socket.emit('error', { message: 'Access denied to team' }); + } + } catch (error) { + socket.emit('error', { message: 'Failed to join team' }); + } + } + + private async getActivitiesForUser(userId: string, filters: any = {}) { + const { teamId, entityType, limit = 50 } = filters; + + const where: any = {}; + + if (teamId) { + where.teamId = teamId; + } else { + // Get activities for tasks/teams user is involved in + const userTeamIds = await this.getUserTeamIds(userId); + + where.OR = [ + { userId }, + { teamId: { in: userTeamIds } }, + { + AND: [ + { entityType: 'TASK' }, + { + task: { + OR: [ + { createdById: userId }, + { assignments: { some: { userId } } }, + { team: { members: { some: { userId } } } } + ] + } + } + ] + } + ]; + } + + if (entityType) { + where.entityType = entityType; + } + + return await prisma.activity.findMany({ + where, + take: limit, + orderBy: { createdAt: 'desc' }, + include: { + user: { + select: { + id: true, + firstName: true, + lastName: true, + email: true + } + }, + team: { + select: { + id: true, + name: true + } + } + } + }); + } + + private async getUserTeamIds(userId: string): Promise { + const memberships = await prisma.teamMember.findMany({ + where: { userId }, + select: { teamId: true } + }); + + return memberships.map(m => m.teamId); + } + + // Public methods for emitting events + public emitToUser(userId: string, event: string, data: any) { + this.io.to(`user:${userId}`).emit(event, data); + } + + public emitToTeam(teamId: string, event: string, data: any) { + this.io.to(`team:${teamId}`).emit(event, data); + } + + public emitToAll(event: string, data: any) { + this.io.emit(event, data); + } + + // Activity broadcasting methods + public broadcastActivity(activity: any) { + // Emit to the user who created the activity + if (activity.userId) { + this.emitToUser(activity.userId, 'new-activity', activity); + } + + // Emit to team members if it's a team activity + if (activity.teamId) { + this.emitToTeam(activity.teamId, 'new-activity', activity); + } + + // For task activities, emit to task assignees and team members + if (activity.entityType === 'TASK' && activity.task) { + // This could be enhanced to get task assignees and emit to them specifically + if (activity.teamId) { + this.emitToTeam(activity.teamId, 'new-activity', activity); + } + } + } + + public broadcastTaskUpdate(task: any) { + // Emit to task creator + if (task.createdById) { + this.emitToUser(task.createdById, 'task-updated', task); + } + + // Emit to task assignees + if (task.assignments) { + task.assignments.forEach((assignment: any) => { + this.emitToUser(assignment.userId, 'task-updated', task); + }); + } + + // Emit to team members + if (task.teamId) { + this.emitToTeam(task.teamId, 'task-updated', task); + } + } + + public broadcastTeamUpdate(team: any) { + if (team.id) { + this.emitToTeam(team.id, 'team-updated', team); + } + } + + public broadcastCommentAdded(comment: any) { + // Emit to task creator + if (comment.task && comment.task.createdById) { + this.emitToUser(comment.task.createdById, 'comment-added', comment); + } + + // Emit to team members if task has a team + if (comment.task && comment.task.teamId) { + this.emitToTeam(comment.task.teamId, 'comment-added', comment); + } + } + + public getIO() { + return this.io; + } +} + +export default SocketService; \ No newline at end of file diff --git a/src/validators/activity.validator.ts b/src/validators/activity.validator.ts new file mode 100755 index 0000000..9f78c12 --- /dev/null +++ b/src/validators/activity.validator.ts @@ -0,0 +1,7 @@ +import Joi from 'joi'; + +export const activityQuerySchema = Joi.object({ + teamId: Joi.string().optional(), + entityType: Joi.string().valid('USER', 'TASK', 'TEAM', 'COMMENT').optional(), + limit: Joi.number().integer().min(1).max(100).optional() +}); \ No newline at end of file diff --git a/src/validators/catagory.validator.js b/src/validators/catagory.validator.js deleted file mode 100644 index 7f1f015..0000000 --- a/src/validators/catagory.validator.js +++ /dev/null @@ -1,113 +0,0 @@ -import Joi from 'joi'; - -/** - * Validation schema for creating a category - */ -export const createCategorySchema = Joi.object({ - name: Joi.string().min(2).max(100).optional().messages({ - 'string.min': 'Category name must be at least 2 characters long', - 'string.max': 'Category name must not exceed 100 characters' - }), - - slug: Joi.string() - .min(2) - .max(100) - .pattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/) - .required() - .messages({ - 'string.min': 'Slug must be at least 2 characters long', - 'string.max': 'Slug must not exceed 100 characters', - 'string.pattern.base': 'Slug must contain only lowercase letters, numbers, and hyphens, and cannot start or end with a hyphen', - 'any.required': 'Slug is required' - }), - - description: Joi.string().min(10).max(500).optional().messages({ - 'string.min': 'Description must be at least 10 characters long', - 'string.max': 'Description must not exceed 500 characters' - }), - - parentId: Joi.string().optional().allow(null).messages({ - 'string.empty': 'Parent ID cannot be empty string' - }) -}); - -/** - * Validation schema for updating a category - */ -export const updateCategorySchema = Joi.object({ - name: Joi.string().min(2).max(100).optional().messages({ - 'string.min': 'Category name must be at least 2 characters long', - 'string.max': 'Category name must not exceed 100 characters' - }), - - slug: Joi.string() - .min(2) - .max(100) - .pattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/) - .optional() - .messages({ - 'string.min': 'Slug must be at least 2 characters long', - 'string.max': 'Slug must not exceed 100 characters', - 'string.pattern.base': 'Slug must contain only lowercase letters, numbers, and hyphens, and cannot start or end with a hyphen' - }), - - description: Joi.string().min(10).max(500).optional().allow(null).messages({ - 'string.min': 'Description must be at least 10 characters long', - 'string.max': 'Description must not exceed 500 characters' - }), - - parentId: Joi.string().optional().allow(null).messages({ - 'string.empty': 'Parent ID cannot be empty string' - }) -}).min(1).messages({ - 'object.min': 'At least one field must be provided for update' -}); - -/** - * Validation schema for category ID parameter - */ -export const categoryIdSchema = Joi.object({ - id: Joi.string().required().messages({ - 'string.empty': 'Category ID is required', - 'any.required': 'Category ID is required' - }) -}); - -/** - * Validation schema for category slug parameter - */ -export const categorySlugSchema = Joi.object({ - slug: Joi.string() - .min(2) - .max(100) - .pattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/) - .required() - .messages({ - 'string.min': 'Slug must be at least 2 characters long', - 'string.max': 'Slug must not exceed 100 characters', - 'string.pattern.base': 'Slug must contain only lowercase letters, numbers, and hyphens, and cannot start or end with a hyphen', - 'any.required': 'Slug is required' - }) -}); - -/** - * Validation schema for search query - */ -export const searchCategoriesSchema = Joi.object({ - q: Joi.string().min(2).max(100).required().messages({ - 'string.min': 'Search term must be at least 2 characters long', - 'string.max': 'Search term must not exceed 100 characters', - 'any.required': 'Search term (q) is required' - }) -}); - -/** - * Validation schema for query parameters - */ -export const categoryQuerySchema = Joi.object({ - parentId: Joi.string().optional(), - includeChildren: Joi.string().valid('true', 'false').optional(), - includeParent: Joi.string().valid('true', 'false').optional(), - includeServices: Joi.string().valid('true', 'false').optional(), - force: Joi.string().valid('true', 'false').optional() -}).unknown(true); // Allow other query parameters diff --git a/src/validators/comment.validator.ts b/src/validators/comment.validator.ts new file mode 100755 index 0000000..046cff9 --- /dev/null +++ b/src/validators/comment.validator.ts @@ -0,0 +1,23 @@ +import Joi from 'joi'; + +export const createCommentSchema = Joi.object({ + content: Joi.string().min(1).max(1000).required().messages({ + 'string.empty': 'Comment content is required', + 'string.max': 'Comment must be less than 1000 characters' + }), + taskId: Joi.string().required().messages({ + 'any.required': 'Task ID is required' + }) +}); + +export const updateCommentSchema = Joi.object({ + content: Joi.string().min(1).max(1000).required().messages({ + 'string.empty': 'Comment content is required', + 'string.max': 'Comment must be less than 1000 characters' + }) +}); + +export const commentQuerySchema = Joi.object({ + page: Joi.number().integer().min(1).optional(), + limit: Joi.number().integer().min(1).max(50).optional() +}); \ No newline at end of file diff --git a/src/validators/company.validator.js b/src/validators/company.validator.js deleted file mode 100644 index a4b3278..0000000 --- a/src/validators/company.validator.js +++ /dev/null @@ -1,19 +0,0 @@ -import Joi from 'joi'; - -export const createCompanySchema = Joi.object({ - name: Joi.string().max(255).required(), - description: Joi.string().max(1000).optional(), - logo: Joi.string().uri().optional(), - address: Joi.string().max(500).optional(), - contact: Joi.string().max(100).optional(), - socialmedia: Joi.array().items(Joi.string().uri()).optional() -}); - -export const updateCompanySchema = Joi.object({ - name: Joi.string().max(255).optional(), - description: Joi.string().max(1000).optional(), - logo: Joi.string().uri().optional(), - address: Joi.string().max(500).optional(), - contact: Joi.string().max(100).optional(), - socialmedia: Joi.array().items(Joi.string().uri()).optional() -}); diff --git a/src/validators/dependency.validator.ts b/src/validators/dependency.validator.ts new file mode 100755 index 0000000..9e572c0 --- /dev/null +++ b/src/validators/dependency.validator.ts @@ -0,0 +1,14 @@ +import Joi from 'joi'; + +export const createDependencySchema = Joi.object({ + taskId: Joi.string().required().messages({ + 'any.required': 'Task ID is required' + }), + dependsOnTaskId: Joi.string().required().messages({ + 'any.required': 'Dependency task ID is required' + }) +}); + +export const dependencyQuerySchema = Joi.object({ + teamId: Joi.string().optional() +}); \ No newline at end of file diff --git a/src/validators/provider.validator.js b/src/validators/provider.validator.js deleted file mode 100644 index 8ba21b6..0000000 --- a/src/validators/provider.validator.js +++ /dev/null @@ -1,17 +0,0 @@ -import Joi from 'joi'; - -export const createProviderSchema = Joi.object({ - bio: Joi.string().max(1000).optional(), - skills: Joi.array().items(Joi.string()).optional(), - qualifications: Joi.array().items(Joi.string()).optional(), - logoUrl: Joi.string().uri().optional(), - IDCardUrl: Joi.string().uri().required() // Required ID card image URL -}); - -export const updateProviderSchema = Joi.object({ - bio: Joi.string().max(1000).optional(), - skills: Joi.array().items(Joi.string()).optional(), - qualifications: Joi.array().items(Joi.string()).optional(), - logoUrl: Joi.string().uri().optional(), - IDCardUrl: Joi.string().uri().optional() // Optional for updates -}); diff --git a/src/validators/services.validator.js b/src/validators/services.validator.js deleted file mode 100644 index b391fe7..0000000 --- a/src/validators/services.validator.js +++ /dev/null @@ -1,121 +0,0 @@ -import Joi from 'joi'; - -/** - * Validation schema for creating a service - */ -export const createServiceSchema = Joi.object({ - providerId: Joi.string().required().messages({ - 'string.empty': 'Provider ID is required', - 'any.required': 'Provider ID is required' - }), - - categoryId: Joi.string().required().messages({ - 'string.empty': 'Category ID is required', - 'any.required': 'Category ID is required' - }), - - title: Joi.string().min(3).max(100).optional().messages({ - 'string.min': 'Title must be at least 3 characters long', - 'string.max': 'Title must not exceed 100 characters' - }), - - description: Joi.string().min(10).max(1000).optional().messages({ - 'string.min': 'Description must be at least 10 characters long', - 'string.max': 'Description must not exceed 1000 characters' - }), - - price: Joi.number().positive().precision(2).required().messages({ - 'number.positive': 'Price must be a positive number', - 'any.required': 'Price is required' - }), - - currency: Joi.string().length(3).uppercase().optional().default('USD').messages({ - 'string.length': 'Currency must be a 3-character code (e.g., USD, EUR)', - 'string.uppercase': 'Currency must be uppercase' - }), - - tags: Joi.array().items(Joi.string().min(2).max(30)).max(10).optional().default([]).messages({ - 'array.max': 'Maximum 10 tags allowed', - 'string.min': 'Each tag must be at least 2 characters long', - 'string.max': 'Each tag must not exceed 30 characters' - }), - - images: Joi.array().items(Joi.string().uri()).max(5).optional().default([]).messages({ - 'array.max': 'Maximum 5 images allowed', - 'string.uri': 'Each image must be a valid URL' - }), - - isActive: Joi.boolean().optional().default(true), - - workingTime: Joi.array().items( - Joi.string().pattern(/^(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday):\s*\d{1,2}:\d{2}\s*(AM|PM)\s*-\s*\d{1,2}:\d{2}\s*(AM|PM)$/i) - ).max(7).optional().default([]).messages({ - 'array.max': 'Maximum 7 working time slots allowed (one per day)', - 'string.pattern.base': 'Working time must be in format "Day: HH:MM AM/PM - HH:MM AM/PM" (e.g., "Monday: 9:00 AM - 5:00 PM")' - }) -}); - -/** - * Validation schema for updating a service - */ -export const updateServiceSchema = Joi.object({ - title: Joi.string().min(3).max(100).optional().messages({ - 'string.min': 'Title must be at least 3 characters long', - 'string.max': 'Title must not exceed 100 characters' - }), - - description: Joi.string().min(10).max(1000).optional().messages({ - 'string.min': 'Description must be at least 10 characters long', - 'string.max': 'Description must not exceed 1000 characters' - }), - - price: Joi.number().positive().precision(2).optional().messages({ - 'number.positive': 'Price must be a positive number' - }), - - currency: Joi.string().length(3).uppercase().optional().messages({ - 'string.length': 'Currency must be a 3-character code (e.g., USD, EUR)', - 'string.uppercase': 'Currency must be uppercase' - }), - - tags: Joi.array().items(Joi.string().min(2).max(30)).max(10).optional().messages({ - 'array.max': 'Maximum 10 tags allowed', - 'string.min': 'Each tag must be at least 2 characters long', - 'string.max': 'Each tag must not exceed 30 characters' - }), - - images: Joi.array().items(Joi.string().uri()).max(5).optional().messages({ - 'array.max': 'Maximum 5 images allowed', - 'string.uri': 'Each image must be a valid URL' - }), - - isActive: Joi.boolean().optional(), - - workingTime: Joi.array().items( - Joi.string().pattern(/^(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday):\s*\d{1,2}:\d{2}\s*(AM|PM)\s*-\s*\d{1,2}:\d{2}\s*(AM|PM)$/i) - ).max(7).optional().messages({ - 'array.max': 'Maximum 7 working time slots allowed (one per day)', - 'string.pattern.base': 'Working time must be in format "Day: HH:MM AM/PM - HH:MM AM/PM" (e.g., "Monday: 9:00 AM - 5:00 PM")' - }) -}); - -/** - * Validation schema for service query parameters - */ -export const getServicesQuerySchema = Joi.object({ - providerId: Joi.string().optional(), - categoryId: Joi.string().optional(), - isActive: Joi.string().valid('true', 'false').optional(), - skip: Joi.number().integer().min(0).optional().default(0), - take: Joi.number().integer().min(1).max(100).optional().default(10) -}); - -/** - * Validation schema for service ID parameter - */ -export const serviceIdSchema = Joi.object({ - id: Joi.string().required().messages({ - 'string.empty': 'Service ID is required', - 'any.required': 'Service ID is required' - }) -}); diff --git a/src/validators/services.validator.ts b/src/validators/services.validator.ts new file mode 100755 index 0000000..afa04b5 --- /dev/null +++ b/src/validators/services.validator.ts @@ -0,0 +1,303 @@ +import Joi from 'joi'; + +/** + * Validation schema for creating a service + */ +export const createServiceSchema = Joi.object({ + providerId: Joi.string().required().messages({ + 'string.empty': 'Provider ID is required', + 'any.required': 'Provider ID is required' + }), + + categoryId: Joi.string().required().messages({ + 'string.empty': 'Category ID is required', + 'any.required': 'Category ID is required' + }), + + title: Joi.string().min(3).max(100).optional().messages({ + 'string.min': 'Title must be at least 3 characters long', + 'string.max': 'Title must not exceed 100 characters' + }), + + description: Joi.string().min(10).max(1000).optional().messages({ + 'string.min': 'Description must be at least 10 characters long', + 'string.max': 'Description must not exceed 1000 characters' + }), + + price: Joi.number().positive().precision(2).required().messages({ + 'number.positive': 'Price must be a positive number', + 'any.required': 'Price is required' + }), + + currency: Joi.string().length(3).uppercase().optional().default('USD').messages({ + 'string.length': 'Currency must be a 3-character code (e.g., USD, EUR)', + 'string.uppercase': 'Currency must be uppercase' + }), + + tags: Joi.array().items(Joi.string().min(2).max(30)).max(10).optional().default([]).messages({ + 'array.max': 'Maximum 10 tags allowed', + 'string.min': 'Each tag must be at least 2 characters long', + 'string.max': 'Each tag must not exceed 30 characters' + }), + + images: Joi.array().items(Joi.string().uri()).max(5).optional().default([]).messages({ + 'array.max': 'Maximum 5 images allowed', + 'string.uri': 'Each image must be a valid URL' + }), + + videoUrl: Joi.string().optional().custom((value, helpers) => { + // Allow empty/null values + if (!value) return value; + + try { + // Try to create a URL object - this will handle most valid URLs including S3 URLs with encoded characters + new URL(value); + return value; + } catch (error) { + // If URL constructor fails, try to URL encode any spaces and special characters + try { + const encodedUrl = value.replace(/\s/g, '%20'); + new URL(encodedUrl); + return value; // Return original value, not encoded + } catch (encodedError) { + return helpers.error('string.uri'); + } + } + }).messages({ + 'string.uri': 'Video URL must be a valid URL' + }), + + isActive: Joi.boolean().optional().default(true), + + workingTime: Joi.array().items( + Joi.string().pattern(/^(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday):\s*\d{1,2}:\d{2}\s*(AM|PM)\s*-\s*\d{1,2}:\d{2}\s*(AM|PM)$/i) + ).max(7).optional().default([]).messages({ + 'array.max': 'Maximum 7 working time slots allowed (one per day)', + 'string.pattern.base': 'Working time must be in format "Day: HH:MM AM/PM - HH:MM AM/PM" (e.g., "Monday: 9:00 AM - 5:00 PM")' + }), + + // Location fields + latitude: Joi.number().min(-90).max(90).optional().messages({ + 'number.min': 'Latitude must be between -90 and 90', + 'number.max': 'Latitude must be between -90 and 90' + }), + + longitude: Joi.number().min(-180).max(180).optional().messages({ + 'number.min': 'Longitude must be between -180 and 180', + 'number.max': 'Longitude must be between -180 and 180' + }), + + address: Joi.string().max(500).allow('').optional().messages({ + 'string.max': 'Address must not exceed 500 characters' + }), + + city: Joi.string().max(100).allow('').optional().messages({ + 'string.max': 'City must not exceed 100 characters' + }), + + state: Joi.string().max(100).allow('').optional().messages({ + 'string.max': 'State must not exceed 100 characters' + }), + + country: Joi.string().max(100).allow('').optional().messages({ + 'string.max': 'Country must not exceed 100 characters' + }), + + postalCode: Joi.string().max(20).allow('').optional().messages({ + 'string.max': 'Postal code must not exceed 20 characters' + }), + + serviceRadiusKm: Joi.number().positive().max(100).optional().default(10).messages({ + 'number.positive': 'Service radius must be a positive number', + 'number.max': 'Service radius cannot exceed 100 km' + }) +}); + +/** + * Validation schema for updating a service + */ +export const updateServiceSchema = Joi.object({ + title: Joi.string().min(3).max(100).optional().messages({ + 'string.min': 'Title must be at least 3 characters long', + 'string.max': 'Title must not exceed 100 characters' + }), + + description: Joi.string().min(10).max(1000).optional().messages({ + 'string.min': 'Description must be at least 10 characters long', + 'string.max': 'Description must not exceed 1000 characters' + }), + + price: Joi.number().positive().precision(2).optional().messages({ + 'number.positive': 'Price must be a positive number' + }), + + currency: Joi.string().length(3).uppercase().optional().messages({ + 'string.length': 'Currency must be a 3-character code (e.g., USD, EUR)', + 'string.uppercase': 'Currency must be uppercase' + }), + + tags: Joi.array().items(Joi.string().min(2).max(30)).max(10).optional().messages({ + 'array.max': 'Maximum 10 tags allowed', + 'string.min': 'Each tag must be at least 2 characters long', + 'string.max': 'Each tag must not exceed 30 characters' + }), + + images: Joi.array().items(Joi.string().uri()).max(5).optional().messages({ + 'array.max': 'Maximum 5 images allowed', + 'string.uri': 'Each image must be a valid URL' + }), + + videoUrl: Joi.string().optional().custom((value, helpers) => { + // Allow empty/null values + if (!value) return value; + + try { + // Try to create a URL object - this will handle most valid URLs including S3 URLs with encoded characters + new URL(value); + return value; + } catch (error) { + // If URL constructor fails, try to URL encode any spaces and special characters + try { + const encodedUrl = value.replace(/\s/g, '%20'); + new URL(encodedUrl); + return value; // Return original value, not encoded + } catch (encodedError) { + return helpers.error('string.uri'); + } + } + }).messages({ + 'string.uri': 'Video URL must be a valid URL' + }), + + isActive: Joi.boolean().optional(), + + workingTime: Joi.array().items( + Joi.string().pattern(/^(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday):\s*\d{1,2}:\d{2}\s*(AM|PM)\s*-\s*\d{1,2}:\d{2}\s*(AM|PM)$/i) + ).max(7).optional().messages({ + 'array.max': 'Maximum 7 working time slots allowed (one per day)', + 'string.pattern.base': 'Working time must be in format "Day: HH:MM AM/PM - HH:MM AM/PM" (e.g., "Monday: 9:00 AM - 5:00 PM")' + }), + + // Location fields (same as create schema) + latitude: Joi.number().min(-90).max(90).optional().messages({ + 'number.min': 'Latitude must be between -90 and 90', + 'number.max': 'Latitude must be between -90 and 90' + }), + + longitude: Joi.number().min(-180).max(180).optional().messages({ + 'number.min': 'Longitude must be between -180 and 180', + 'number.max': 'Longitude must be between -180 and 180' + }), + + address: Joi.string().max(500).allow('').optional().messages({ + 'string.max': 'Address must not exceed 500 characters' + }), + + city: Joi.string().max(100).allow('').optional().messages({ + 'string.max': 'City must not exceed 100 characters' + }), + + state: Joi.string().max(100).allow('').optional().messages({ + 'string.max': 'State must not exceed 100 characters' + }), + + country: Joi.string().max(100).allow('').optional().messages({ + 'string.max': 'Country must not exceed 100 characters' + }), + + postalCode: Joi.string().max(20).allow('').optional().messages({ + 'string.max': 'Postal code must not exceed 20 characters' + }), + + serviceRadiusKm: Joi.number().positive().max(100).optional().messages({ + 'number.positive': 'Service radius must be a positive number', + 'number.max': 'Service radius cannot exceed 100 km' + }) +}); + +/** + * Validation schema for service query parameters + */ +export const getServicesQuerySchema = Joi.object({ + providerId: Joi.string().optional(), + categoryId: Joi.string().optional(), + isActive: Joi.string().valid('true', 'false').optional(), + skip: Joi.number().integer().min(0).optional().default(0), + take: Joi.number().integer().min(1).max(100).optional().default(10) +}); + +/** + * Validation schema for service ID parameter + */ +export const serviceIdSchema = Joi.object({ + id: Joi.string().required().messages({ + 'string.empty': 'Service ID is required', + 'any.required': 'Service ID is required' + }) +}); + +/** + * Validation schema for conversation ID parameter + */ +export const conversationIdSchema = Joi.object({ + conversationId: Joi.string().required().messages({ + 'string.empty': 'Conversation ID is required', + 'any.required': 'Conversation ID is required' + }) +}); + +/** + * Validation schema for location-based service search + */ +export const searchServicesByLocationSchema = Joi.object({ + lat: Joi.number().min(-90).max(90).required().messages({ + 'number.min': 'Latitude must be between -90 and 90', + 'number.max': 'Latitude must be between -90 and 90', + 'any.required': 'Latitude is required' + }), + lng: Joi.number().min(-180).max(180).required().messages({ + 'number.min': 'Longitude must be between -180 and 180', + 'number.max': 'Longitude must be between -180 and 180', + 'any.required': 'Longitude is required' + }), + radius: Joi.number().positive().max(100).optional().default(10).messages({ + 'number.positive': 'Radius must be a positive number', + 'number.max': 'Radius cannot exceed 100 km' + }), + categoryId: Joi.string().optional(), + skip: Joi.number().integer().min(0).optional().default(0), + take: Joi.number().integer().min(1).max(100).optional().default(10) +}); + +/** + * Validation schema for geocoding address + */ +export const geocodeAddressSchema = Joi.object({ + address: Joi.string().min(5).max(500).required().messages({ + 'string.min': 'Address must be at least 5 characters long', + 'string.max': 'Address must not exceed 500 characters', + 'any.required': 'Address is required' + }) +}); + +/** + * Validation schema for reverse geocoding coordinates + */ +export const reverseGeocodeSchema = Joi.object({ + lat: Joi.number().min(-90).max(90).optional().messages({ + 'number.min': 'Latitude must be between -90 and 90', + 'number.max': 'Latitude must be between -90 and 90' + }), + lng: Joi.number().min(-180).max(180).optional().messages({ + 'number.min': 'Longitude must be between -180 and 180', + 'number.max': 'Longitude must be between -180 and 180' + }), + latitude: Joi.number().min(-90).max(90).optional().messages({ + 'number.min': 'Latitude must be between -90 and 90', + 'number.max': 'Latitude must be between -90 and 90' + }), + longitude: Joi.number().min(-180).max(180).optional().messages({ + 'number.min': 'Longitude must be between -180 and 180', + 'number.max': 'Longitude must be between -180 and 180' + }) +}).or('lat', 'latitude').or('lng', 'longitude'); diff --git a/src/validators/task.validator.ts b/src/validators/task.validator.ts new file mode 100755 index 0000000..c206c11 --- /dev/null +++ b/src/validators/task.validator.ts @@ -0,0 +1,43 @@ +import Joi from 'joi'; + +export const createTaskSchema = Joi.object({ + title: Joi.string().min(1).max(200).required().messages({ + 'string.empty': 'Task title is required', + 'string.max': 'Task title must be less than 200 characters' + }), + description: Joi.string().max(2000).optional().allow(''), + priority: Joi.string().valid('LOW', 'MEDIUM', 'HIGH', 'URGENT').optional(), + dueDate: Joi.date().optional().allow(null).messages({ + 'date.base': 'Due date must be a valid date' + }), + teamId: Joi.string().optional().allow(''), + assigneeIds: Joi.array().items(Joi.string()).optional() +}); + +export const updateTaskSchema = Joi.object({ + title: Joi.string().min(1).max(200).optional(), + description: Joi.string().max(2000).optional().allow(''), + status: Joi.string().valid('TODO', 'IN_PROGRESS', 'IN_REVIEW', 'DONE', 'CANCELLED').optional(), + priority: Joi.string().valid('LOW', 'MEDIUM', 'HIGH', 'URGENT').optional(), + dueDate: Joi.date().optional().allow(null) +}); + +export const assignTaskSchema = Joi.object({ + userIds: Joi.array().items(Joi.string()).required().messages({ + 'array.base': 'userIds must be an array', + 'any.required': 'userIds is required' + }) +}); + +export const taskQuerySchema = Joi.object({ + status: Joi.string().valid('TODO', 'IN_PROGRESS', 'IN_REVIEW', 'DONE', 'CANCELLED').optional(), + priority: Joi.string().valid('LOW', 'MEDIUM', 'HIGH', 'URGENT').optional(), + assigneeId: Joi.string().optional(), + teamId: Joi.string().optional(), + createdById: Joi.string().optional(), + dueDateBefore: Joi.date().optional(), + dueDateAfter: Joi.date().optional(), + search: Joi.string().max(100).optional(), + page: Joi.number().integer().min(1).optional(), + limit: Joi.number().integer().min(1).max(100).optional() +}); \ No newline at end of file diff --git a/src/validators/team.validator.ts b/src/validators/team.validator.ts new file mode 100755 index 0000000..2c67f72 --- /dev/null +++ b/src/validators/team.validator.ts @@ -0,0 +1,28 @@ +import Joi from 'joi'; + +export const createTeamSchema = Joi.object({ + name: Joi.string().min(1).max(100).required().messages({ + 'string.empty': 'Team name is required', + 'string.max': 'Team name must be less than 100 characters' + }), + description: Joi.string().max(500).optional().allow('') +}); + +export const updateTeamSchema = Joi.object({ + name: Joi.string().min(1).max(100).optional(), + description: Joi.string().max(500).optional().allow('') +}); + +export const addTeamMemberSchema = Joi.object({ + userId: Joi.string().required().messages({ + 'any.required': 'User ID is required' + }), + role: Joi.string().valid('OWNER', 'ADMIN', 'MEMBER').optional() +}); + +export const updateTeamMemberRoleSchema = Joi.object({ + role: Joi.string().valid('ADMIN', 'MEMBER').required().messages({ + 'any.required': 'Role is required', + 'any.only': 'Role must be either ADMIN or MEMBER' + }) +}); \ No newline at end of file diff --git a/src/validators/user.validator.js b/src/validators/user.validator.ts old mode 100644 new mode 100755 similarity index 71% rename from src/validators/user.validator.js rename to src/validators/user.validator.ts index 6f3497d..a41ab18 --- a/src/validators/user.validator.js +++ b/src/validators/user.validator.ts @@ -5,11 +5,8 @@ export const registerSchema = Joi.object({ lastName: Joi.string().required(), email: Joi.string().email().required(), password: Joi.string().min(6).required(), - imageUrl: Joi.string().uri().optional(), - location: Joi.string().optional(), address: Joi.string().optional(), phone: Joi.string().pattern(/^[0-9]{11}$/).optional(), - socialmedia: Joi.array().items(Joi.string()).optional() }); export const loginSchema = Joi.object({ @@ -20,9 +17,6 @@ export const loginSchema = Joi.object({ export const updateProfileSchema = Joi.object({ firstName: Joi.string().optional(), lastName: Joi.string().optional(), - imageUrl: Joi.string().uri().optional(), - location: Joi.string().optional(), address: Joi.string().optional(), phone: Joi.string().pattern(/^[0-9]{11}$/).optional(), - socialmedia: Joi.array().items(Joi.string()).optional() }); diff --git a/test-runner.js b/test-runner.js new file mode 100755 index 0000000..54e2580 --- /dev/null +++ b/test-runner.js @@ -0,0 +1,272 @@ +#!/usr/bin/env node + +/** + * Test Runner Script for Task Management System + * + * This script sets up the test environment, runs migrations, + * executes tests, and generates coverage reports. + */ + +const { exec } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +// Test configuration +const TEST_CONFIG = { + testTimeout: 30000, + maxWorkers: 1, // Use single worker for database tests + setupTimeout: 60000, + coverageThreshold: { + global: { + branches: 80, + functions: 80, + lines: 80, + statements: 80 + } + } +}; + +// Colors for console output +const colors = { + reset: '\x1b[0m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + magenta: '\x1b[35m', + cyan: '\x1b[36m' +}; + +function log(message, color = 'reset') { + console.log(`${colors[color]}${message}${colors.reset}`); +} + +function executeCommand(command) { + return new Promise((resolve, reject) => { + log(`Executing: ${command}`, 'cyan'); + exec(command, (error, stdout, stderr) => { + if (error) { + log(`Error: ${error.message}`, 'red'); + reject(error); + return; + } + if (stderr) { + log(`Warning: ${stderr}`, 'yellow'); + } + if (stdout) { + console.log(stdout); + } + resolve(stdout); + }); + }); +} + +async function checkTestDatabase() { + log('🔍 Checking test database configuration...', 'blue'); + + const envTestPath = path.join(__dirname, '.env.test'); + if (!fs.existsSync(envTestPath)) { + log('❌ .env.test file not found!', 'red'); + log('Please create .env.test with TEST_DATABASE_URL', 'yellow'); + process.exit(1); + } + + log('✅ Test environment configuration found', 'green'); +} + +async function setupTestDatabase() { + log('đŸ› ī¸ Setting up test database...', 'blue'); + + try { + // Run Prisma migrations for test database + await executeCommand('npx dotenv -e .env.test -- npx prisma migrate deploy'); + + // Generate Prisma client + await executeCommand('npx prisma generate'); + + log('✅ Test database setup complete', 'green'); + } catch (error) { + log('❌ Failed to setup test database', 'red'); + throw error; + } +} + +async function runLinting() { + log('🔍 Running code linting...', 'blue'); + + try { + // Check if ESLint is available + await executeCommand('npx eslint --version'); + await executeCommand('npx eslint src/ tests/ --ext .ts'); + log('✅ Linting passed', 'green'); + } catch (error) { + log('âš ī¸ Linting skipped (ESLint not configured)', 'yellow'); + } +} + +async function runTypeChecking() { + log('🔍 Running TypeScript type checking...', 'blue'); + + try { + await executeCommand('npx tsc --noEmit'); + log('✅ Type checking passed', 'green'); + } catch (error) { + log('❌ Type checking failed', 'red'); + throw error; + } +} + +async function runTests(options = {}) { + log('đŸ§Ē Running tests...', 'blue'); + + const { + coverage = false, + watch = false, + pattern = '', + verbose = false + } = options; + + let command = 'npx dotenv -e .env.test -- npx jest'; + + if (coverage) { + command += ' --coverage'; + } + + if (watch) { + command += ' --watch'; + } + + if (pattern) { + command += ` --testNamePattern="${pattern}"`; + } + + if (verbose) { + command += ' --verbose'; + } + + // Add test configuration + command += ` --maxWorkers=${TEST_CONFIG.maxWorkers}`; + command += ` --testTimeout=${TEST_CONFIG.testTimeout}`; + + try { + await executeCommand(command); + log('✅ All tests passed!', 'green'); + } catch (error) { + log('❌ Some tests failed', 'red'); + throw error; + } +} + +async function generateCoverageReport() { + log('📊 Generating coverage report...', 'blue'); + + try { + await executeCommand('npx dotenv -e .env.test -- npx jest --coverage --coverageReporters=html --coverageReporters=text'); + + const coveragePath = path.join(__dirname, 'coverage', 'lcov-report', 'index.html'); + if (fs.existsSync(coveragePath)) { + log(`📄 Coverage report generated: ${coveragePath}`, 'green'); + } + } catch (error) { + log('❌ Failed to generate coverage report', 'red'); + throw error; + } +} + +async function cleanupTestData() { + log('🧹 Cleaning up test data...', 'blue'); + + try { + // Reset test database + await executeCommand('npx dotenv -e .env.test -- npx prisma migrate reset --force'); + log('✅ Test data cleaned up', 'green'); + } catch (error) { + log('âš ī¸ Failed to cleanup test data', 'yellow'); + } +} + +async function runTestSuite(options = {}) { + const startTime = Date.now(); + + try { + log('🚀 Starting Task Management System Test Suite', 'magenta'); + log('=' * 50, 'cyan'); + + // Check prerequisites + await checkTestDatabase(); + + // Setup + await setupTestDatabase(); + + // Code quality checks + if (!options.skipLinting) { + await runLinting(); + } + + if (!options.skipTypeCheck) { + await runTypeChecking(); + } + + // Run tests + await runTests(options); + + // Generate reports + if (options.coverage) { + await generateCoverageReport(); + } + + // Cleanup + if (options.cleanup) { + await cleanupTestData(); + } + + const duration = Math.round((Date.now() - startTime) / 1000); + log(`🎉 Test suite completed successfully in ${duration}s`, 'green'); + + } catch (error) { + const duration = Math.round((Date.now() - startTime) / 1000); + log(`đŸ’Ĩ Test suite failed after ${duration}s`, 'red'); + process.exit(1); + } +} + +// CLI Interface +const args = process.argv.slice(2); +const options = { + coverage: args.includes('--coverage'), + watch: args.includes('--watch'), + verbose: args.includes('--verbose'), + skipLinting: args.includes('--skip-lint'), + skipTypeCheck: args.includes('--skip-type-check'), + cleanup: args.includes('--cleanup'), + pattern: args.find(arg => arg.startsWith('--pattern='))?.split('=')[1] || '' +}; + +// Help message +if (args.includes('--help') || args.includes('-h')) { + console.log(` +Task Management System Test Runner + +Usage: node test-runner.js [options] + +Options: + --coverage Generate coverage report + --watch Run tests in watch mode + --verbose Show verbose test output + --skip-lint Skip linting checks + --skip-type-check Skip TypeScript type checking + --cleanup Clean up test data after tests + --pattern=PATTERN Run tests matching pattern + --help, -h Show this help message + +Examples: + node test-runner.js --coverage + node test-runner.js --watch + node test-runner.js --pattern="User.*should.*" + node test-runner.js --coverage --cleanup +`); + process.exit(0); +} + +// Run the test suite +runTestSuite(options); \ No newline at end of file diff --git a/tests/activity.test.ts b/tests/activity.test.ts new file mode 100755 index 0000000..731ab6e --- /dev/null +++ b/tests/activity.test.ts @@ -0,0 +1,510 @@ +import { ActivityType, EntityType } from '@prisma/client'; +import express from 'express'; +import request from 'supertest'; +import { errorHandler } from '../src/middlewares/error.middleware'; +import activityRoutes from '../src/routes/activity.routes'; +import { createAuthHeaders, createTestTask, createTestTeam, createTestUser, generateAuthToken } from './helpers'; +import { prisma } from './setup'; + +const app = express(); +app.use(express.json()); +app.use('/api/activities', activityRoutes); +app.use(errorHandler); + +describe('Activity Tracking', () => { + let user: any; + let token: string; + let team: any; + let task: any; + + beforeEach(async () => { + user = await createTestUser(); + token = generateAuthToken(user.id); + team = await createTestTeam(user.id); + task = await createTestTask(user.id, { teamId: team.id }); + }); + + describe('Activity Logging', () => { + it('should log task creation activity', async () => { + // Check if activity was automatically logged when task was created + const activities = await prisma.activity.findMany({ + where: { + type: ActivityType.TASK_CREATED, + entityType: EntityType.TASK, + entityId: task.id, + }, + }); + + expect(activities.length).toBeGreaterThan(0); + expect(activities[0].userId).toBe(user.id); + expect(activities[0].teamId).toBe(team.id); + }); + + it('should log task assignment activity', async () => { + const assignee = await createTestUser({ email: 'assignee@example.com' }); + + await request(app) + .post(`/api/tasks/${task.id}/assign/${assignee.id}`) + .set(createAuthHeaders(token)) + .expect(200); + + const activities = await prisma.activity.findMany({ + where: { + type: ActivityType.TASK_ASSIGNED, + entityType: EntityType.TASK, + entityId: task.id, + }, + }); + + expect(activities.length).toBeGreaterThan(0); + expect(activities[0].userId).toBe(user.id); // User who performed the assignment + }); + + it('should log task completion activity', async () => { + await request(app) + .put(`/api/tasks/${task.id}/status`) + .set(createAuthHeaders(token)) + .send({ status: 'DONE' }) + .expect(200); + + const activities = await prisma.activity.findMany({ + where: { + type: ActivityType.TASK_COMPLETED, + entityType: EntityType.TASK, + entityId: task.id, + }, + }); + + expect(activities.length).toBeGreaterThan(0); + }); + + it('should log team creation activity', async () => { + const teamData = { + name: 'New Activity Test Team', + description: 'Team for activity testing', + }; + + const response = await request(app) + .post('/api/teams') + .set(createAuthHeaders(token)) + .send(teamData) + .expect(201); + + const activities = await prisma.activity.findMany({ + where: { + type: ActivityType.TEAM_CREATED, + entityType: EntityType.TEAM, + entityId: response.body.data.id, + }, + }); + + expect(activities.length).toBeGreaterThan(0); + }); + + it('should log comment creation activity', async () => { + const commentData = { + content: 'Test comment for activity logging', + }; + + const response = await request(app) + .post(`/api/tasks/${task.id}/comments`) + .set(createAuthHeaders(token)) + .send(commentData) + .expect(201); + + const activities = await prisma.activity.findMany({ + where: { + type: ActivityType.TASK_COMMENTED, + entityType: EntityType.COMMENT, + }, + }); + + expect(activities.length).toBeGreaterThan(0); + }); + }); + + describe('GET /api/activities', () => { + beforeEach(async () => { + // Create some test activities + await prisma.activity.createMany({ + data: [ + { + type: ActivityType.TASK_CREATED, + description: 'Created a new task', + entityType: EntityType.TASK, + entityId: task.id, + userId: user.id, + teamId: team.id, + }, + { + type: ActivityType.TASK_UPDATED, + description: 'Updated task details', + entityType: EntityType.TASK, + entityId: task.id, + userId: user.id, + teamId: team.id, + }, + { + type: ActivityType.TEAM_CREATED, + description: 'Created a new team', + entityType: EntityType.TEAM, + entityId: team.id, + userId: user.id, + }, + ], + }); + }); + + it('should get user activities with pagination', async () => { + const response = await request(app) + .get('/api/activities') + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data).toBeInstanceOf(Array); + expect(response.body.data.length).toBeGreaterThan(0); + expect(response.body.pagination).toBeDefined(); + }); + + it('should filter activities by type', async () => { + const response = await request(app) + .get('/api/activities') + .query({ type: ActivityType.TASK_CREATED }) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data.every((activity: any) => + activity.type === ActivityType.TASK_CREATED + )).toBe(true); + }); + + it('should filter activities by entity type', async () => { + const response = await request(app) + .get('/api/activities') + .query({ entityType: EntityType.TASK }) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data.every((activity: any) => + activity.entityType === EntityType.TASK + )).toBe(true); + }); + + it('should filter activities by date range', async () => { + const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000); + const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000); + + const response = await request(app) + .get('/api/activities') + .query({ + startDate: yesterday.toISOString(), + endDate: tomorrow.toISOString() + }) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data).toBeInstanceOf(Array); + }); + + it('should sort activities by creation date', async () => { + const response = await request(app) + .get('/api/activities') + .query({ sortBy: 'createdAt', sortOrder: 'desc' }) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.success).toBe(true); + + if (response.body.data.length > 1) { + const activities = response.body.data; + for (let i = 0; i < activities.length - 1; i++) { + const current = new Date(activities[i].createdAt); + const next = new Date(activities[i + 1].createdAt); + expect(current.getTime()).toBeGreaterThanOrEqual(next.getTime()); + } + } + }); + }); + + describe('GET /api/activities/team/:teamId', () => { + it('should get team activities', async () => { + const response = await request(app) + .get(`/api/activities/team/${team.id}`) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data).toBeInstanceOf(Array); + expect(response.body.data.every((activity: any) => + activity.teamId === team.id + )).toBe(true); + }); + + it('should require team membership to view team activities', async () => { + const outsider = await createTestUser({ email: 'outsider@example.com' }); + const outsiderToken = generateAuthToken(outsider.id); + + const response = await request(app) + .get(`/api/activities/team/${team.id}`) + .set(createAuthHeaders(outsiderToken)) + .expect(403); + + expect(response.body.success).toBe(false); + }); + }); + + describe('GET /api/activities/task/:taskId', () => { + it('should get task-specific activities', async () => { + const response = await request(app) + .get(`/api/activities/task/${task.id}`) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data).toBeInstanceOf(Array); + expect(response.body.data.every((activity: any) => + activity.entityId === task.id || activity.entityType === EntityType.TASK + )).toBe(true); + }); + + it('should include related activities (comments, assignments)', async () => { + // Create a comment on the task + await prisma.comment.create({ + data: { + content: 'Test comment', + taskId: task.id, + userId: user.id, + }, + }); + + // Log comment activity + await prisma.activity.create({ + data: { + type: ActivityType.TASK_COMMENTED, + description: 'Added a comment', + entityType: EntityType.COMMENT, + entityId: 'comment-id', + userId: user.id, + teamId: team.id, + metadata: { taskId: task.id }, + }, + }); + + const response = await request(app) + .get(`/api/activities/task/${task.id}`) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data.some((activity: any) => + activity.type === ActivityType.TASK_COMMENTED + )).toBe(true); + }); + }); + + describe('Activity Metadata and Context', () => { + it('should include metadata in activities', async () => { + const activityWithMetadata = await prisma.activity.create({ + data: { + type: ActivityType.TASK_UPDATED, + description: 'Updated task priority', + entityType: EntityType.TASK, + entityId: task.id, + userId: user.id, + teamId: team.id, + metadata: { + previousPriority: 'MEDIUM', + newPriority: 'HIGH', + field: 'priority', + }, + }, + }); + + const response = await request(app) + .get('/api/activities') + .set(createAuthHeaders(token)) + .expect(200); + + const activity = response.body.data.find((a: any) => a.id === activityWithMetadata.id); + expect(activity.metadata).toBeDefined(); + expect(activity.metadata.previousPriority).toBe('MEDIUM'); + expect(activity.metadata.newPriority).toBe('HIGH'); + }); + + it('should include user information in activities', async () => { + const response = await request(app) + .get('/api/activities') + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.success).toBe(true); + if (response.body.data.length > 0) { + const activity = response.body.data[0]; + expect(activity.user).toBeDefined(); + expect(activity.user.firstName).toBeDefined(); + expect(activity.user.lastName).toBeDefined(); + expect(activity.user.password).toBeUndefined(); // Should not include password + } + }); + }); + + describe('Activity Aggregation and Analytics', () => { + beforeEach(async () => { + // Create various activities for analytics + const activities = [ + { type: ActivityType.TASK_CREATED, entityType: EntityType.TASK }, + { type: ActivityType.TASK_CREATED, entityType: EntityType.TASK }, + { type: ActivityType.TASK_COMPLETED, entityType: EntityType.TASK }, + { type: ActivityType.TEAM_CREATED, entityType: EntityType.TEAM }, + { type: ActivityType.TASK_COMMENTED, entityType: EntityType.COMMENT }, + { type: ActivityType.TASK_COMMENTED, entityType: EntityType.COMMENT }, + ]; + + await prisma.activity.createMany({ + data: activities.map(activity => ({ + ...activity, + description: `Test ${activity.type}`, + entityId: 'test-entity', + userId: user.id, + teamId: team.id, + })), + }); + }); + + it('should get activity statistics', async () => { + const response = await request(app) + .get('/api/activities/stats') + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data.totalActivities).toBeDefined(); + expect(response.body.data.byType).toBeDefined(); + expect(response.body.data.byType[ActivityType.TASK_CREATED]).toBe(2); + expect(response.body.data.byType[ActivityType.TASK_COMPLETED]).toBe(1); + }); + + it('should get team activity statistics', async () => { + const response = await request(app) + .get(`/api/activities/stats/team/${team.id}`) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data.teamId).toBe(team.id); + expect(response.body.data.totalActivities).toBeGreaterThan(0); + }); + + it('should get activity timeline', async () => { + const response = await request(app) + .get('/api/activities/timeline') + .query({ groupBy: 'day' }) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data).toBeInstanceOf(Array); + if (response.body.data.length > 0) { + expect(response.body.data[0].date).toBeDefined(); + expect(response.body.data[0].count).toBeDefined(); + } + }); + }); + + describe('Performance and Optimization', () => { + it('should handle large activity datasets efficiently', async () => { + // Create a large number of activities + const activities = Array.from({ length: 100 }, (_, i) => ({ + type: ActivityType.TASK_CREATED, + description: `Bulk activity ${i}`, + entityType: EntityType.TASK, + entityId: `task-${i}`, + userId: user.id, + teamId: team.id, + })); + + await prisma.activity.createMany({ data: activities }); + + const startTime = Date.now(); + const response = await request(app) + .get('/api/activities') + .query({ limit: 50 }) + .set(createAuthHeaders(token)) + .expect(200); + const endTime = Date.now(); + + expect(response.body.success).toBe(true); + expect(endTime - startTime).toBeLessThan(1000); // Should complete within 1 second + expect(response.body.data.length).toBeLessThanOrEqual(50); + }); + + it('should implement proper pagination for activities', async () => { + const response1 = await request(app) + .get('/api/activities') + .query({ page: 1, limit: 5 }) + .set(createAuthHeaders(token)) + .expect(200); + + const response2 = await request(app) + .get('/api/activities') + .query({ page: 2, limit: 5 }) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response1.body.data.length).toBeLessThanOrEqual(5); + expect(response2.body.data.length).toBeLessThanOrEqual(5); + + // Should have different data (if enough activities exist) + if (response1.body.data.length === 5 && response2.body.data.length > 0) { + expect(response1.body.data[0].id).not.toBe(response2.body.data[0].id); + } + }); + }); + + describe('Error Handling and Edge Cases', () => { + it('should handle invalid activity type filters', async () => { + const response = await request(app) + .get('/api/activities') + .query({ type: 'INVALID_TYPE' }) + .set(createAuthHeaders(token)) + .expect(400); + + expect(response.body.success).toBe(false); + }); + + it('should handle invalid date ranges', async () => { + const response = await request(app) + .get('/api/activities') + .query({ + startDate: 'invalid-date', + endDate: 'also-invalid' + }) + .set(createAuthHeaders(token)) + .expect(400); + + expect(response.body.success).toBe(false); + }); + + it('should handle non-existent team activities', async () => { + const response = await request(app) + .get('/api/activities/team/non-existent-team-id') + .set(createAuthHeaders(token)) + .expect(404); + + expect(response.body.success).toBe(false); + }); + + it('should handle non-existent task activities', async () => { + const response = await request(app) + .get('/api/activities/task/non-existent-task-id') + .set(createAuthHeaders(token)) + .expect(404); + + expect(response.body.success).toBe(false); + }); + }); +}); \ No newline at end of file diff --git a/tests/comment.test.ts b/tests/comment.test.ts new file mode 100755 index 0000000..47dc970 --- /dev/null +++ b/tests/comment.test.ts @@ -0,0 +1,383 @@ +import request from 'supertest'; +import express from 'express'; +import commentRoutes from '../src/routes/comment.routes'; +import { errorHandler } from '../src/middlewares/error.middleware'; +import { prisma } from './setup'; +import { createTestUser, createTestTask, generateAuthToken, createAuthHeaders } from './helpers'; + +const app = express(); +app.use(express.json()); +app.use('/api/comments', commentRoutes); +app.use(errorHandler); + +describe('Comment Management', () => { + let user: any; + let otherUser: any; + let token: string; + let otherToken: string; + let task: any; + + beforeEach(async () => { + user = await createTestUser(); + otherUser = await createTestUser({ email: 'other@example.com' }); + token = generateAuthToken(user.id); + otherToken = generateAuthToken(otherUser.id); + task = await createTestTask(user.id); + }); + + describe('POST /api/comments', () => { + it('should create a comment successfully', async () => { + const commentData = { + content: 'This is a test comment', + taskId: task.id, + }; + + const response = await request(app) + .post('/api/comments') + .set(createAuthHeaders(token)) + .send(commentData) + .expect(201); + + expect(response.body).toMatchObject({ + success: true, + data: { + content: commentData.content, + taskId: task.id, + authorId: user.id, + }, + }); + + expect(response.body.data.id).toBeDefined(); + expect(response.body.data.createdAt).toBeDefined(); + }); + + it('should validate required fields', async () => { + const invalidCommentData = { + taskId: task.id, + // Missing content + }; + + const response = await request(app) + .post('/api/comments') + .set(createAuthHeaders(token)) + .send(invalidCommentData) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('validation'); + }); + + it('should validate task exists', async () => { + const commentData = { + content: 'This is a test comment', + taskId: 'non-existent-task-id', + }; + + const response = await request(app) + .post('/api/comments') + .set(createAuthHeaders(token)) + .send(commentData) + .expect(404); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('not found'); + }); + + it('should require authentication', async () => { + const commentData = { + content: 'This is a test comment', + taskId: task.id, + }; + + const response = await request(app) + .post('/api/comments') + .send(commentData) + .expect(401); + + expect(response.body.success).toBe(false); + }); + }); + + describe('GET /api/comments/task/:taskId', () => { + it('should get task comments successfully', async () => { + // Create test comments + const comment1 = await prisma.comment.create({ + data: { + content: 'First comment', + taskId: task.id, + authorId: user.id, + }, + }); + + const comment2 = await prisma.comment.create({ + data: { + content: 'Second comment', + taskId: task.id, + authorId: otherUser.id, + }, + }); + + const response = await request(app) + .get(`/api/comments/task/${task.id}`) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body).toMatchObject({ + success: true, + data: expect.any(Array), + }); + + expect(response.body.data).toHaveLength(2); + + // Check comments include author information + const comments = response.body.data; + expect(comments[0].author).toBeDefined(); + expect(comments[0].author.firstName).toBeDefined(); + expect(comments[0].author.password).toBeUndefined(); + }); + + it('should return empty array for task with no comments', async () => { + const newTask = await createTestTask(user.id, { title: 'No Comments Task' }); + + const response = await request(app) + .get(`/api/comments/task/${newTask.id}`) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body).toMatchObject({ + success: true, + data: [], + }); + }); + + it('should support pagination', async () => { + // Create multiple comments + for (let i = 1; i <= 15; i++) { + await prisma.comment.create({ + data: { + content: `Comment ${i}`, + taskId: task.id, + authorId: user.id, + }, + }); + } + + const response = await request(app) + .get(`/api/comments/task/${task.id}?page=1&limit=10`) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.data).toHaveLength(10); + }); + + it('should order comments by creation date (newest first)', async () => { + // Create comments with delays to ensure different timestamps + const comment1 = await prisma.comment.create({ + data: { + content: 'First comment', + taskId: task.id, + authorId: user.id, + }, + }); + + await new Promise(resolve => setTimeout(resolve, 10)); + + const comment2 = await prisma.comment.create({ + data: { + content: 'Second comment', + taskId: task.id, + authorId: user.id, + }, + }); + + const response = await request(app) + .get(`/api/comments/task/${task.id}`) + .set(createAuthHeaders(token)) + .expect(200); + + const comments = response.body.data; + expect(new Date(comments[0].createdAt).getTime()) + .toBeGreaterThan(new Date(comments[1].createdAt).getTime()); + }); + }); + + describe('PUT /api/comments/:id', () => { + it('should update comment successfully', async () => { + const comment = await prisma.comment.create({ + data: { + content: 'Original content', + taskId: task.id, + authorId: user.id, + }, + }); + + const updateData = { + content: 'Updated content', + }; + + const response = await request(app) + .put(`/api/comments/${comment.id}`) + .set(createAuthHeaders(token)) + .send(updateData) + .expect(200); + + expect(response.body).toMatchObject({ + success: true, + data: { + id: comment.id, + content: updateData.content, + updatedAt: expect.any(String), + }, + }); + + // Verify update in database + const updatedComment = await prisma.comment.findUnique({ + where: { id: comment.id }, + }); + + expect(updatedComment?.content).toBe(updateData.content); + expect(updatedComment?.updatedAt.getTime()).toBeGreaterThan(comment.createdAt.getTime()); + }); + + it('should deny access to non-author', async () => { + const comment = await prisma.comment.create({ + data: { + content: 'Original content', + taskId: task.id, + authorId: user.id, + }, + }); + + const updateData = { + content: 'Hacked content', + }; + + const response = await request(app) + .put(`/api/comments/${comment.id}`) + .set(createAuthHeaders(otherToken)) + .send(updateData) + .expect(403); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('permission'); + }); + + it('should validate updated content', async () => { + const comment = await prisma.comment.create({ + data: { + content: 'Original content', + taskId: task.id, + authorId: user.id, + }, + }); + + const invalidData = { + content: '', // Empty content + }; + + const response = await request(app) + .put(`/api/comments/${comment.id}`) + .set(createAuthHeaders(token)) + .send(invalidData) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('validation'); + }); + + it('should return 404 for non-existent comment', async () => { + const updateData = { + content: 'Updated content', + }; + + const response = await request(app) + .put('/api/comments/non-existent-id') + .set(createAuthHeaders(token)) + .send(updateData) + .expect(404); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('not found'); + }); + }); + + describe('DELETE /api/comments/:id', () => { + it('should delete comment successfully', async () => { + const comment = await prisma.comment.create({ + data: { + content: 'Comment to delete', + taskId: task.id, + authorId: user.id, + }, + }); + + const response = await request(app) + .delete(`/api/comments/${comment.id}`) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body).toMatchObject({ + success: true, + message: 'Comment deleted successfully', + }); + + // Verify deletion in database + const deletedComment = await prisma.comment.findUnique({ + where: { id: comment.id }, + }); + + expect(deletedComment).toBeNull(); + }); + + it('should deny access to non-author', async () => { + const comment = await prisma.comment.create({ + data: { + content: 'Comment to delete', + taskId: task.id, + authorId: user.id, + }, + }); + + const response = await request(app) + .delete(`/api/comments/${comment.id}`) + .set(createAuthHeaders(otherToken)) + .expect(403); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('permission'); + + // Verify comment still exists + const existingComment = await prisma.comment.findUnique({ + where: { id: comment.id }, + }); + + expect(existingComment).toBeTruthy(); + }); + + it('should return 404 for non-existent comment', async () => { + const response = await request(app) + .delete('/api/comments/non-existent-id') + .set(createAuthHeaders(token)) + .expect(404); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('not found'); + }); + + it('should require authentication', async () => { + const comment = await prisma.comment.create({ + data: { + content: 'Comment to delete', + taskId: task.id, + authorId: user.id, + }, + }); + + const response = await request(app) + .delete(`/api/comments/${comment.id}`) + .expect(401); + + expect(response.body.success).toBe(false); + }); + }); +}); \ No newline at end of file diff --git a/tests/dependency.test.ts b/tests/dependency.test.ts new file mode 100755 index 0000000..584f224 --- /dev/null +++ b/tests/dependency.test.ts @@ -0,0 +1,338 @@ +import request from 'supertest'; +import express from 'express'; +import dependencyRoutes from '../src/routes/dependency.routes'; +import { errorHandler } from '../src/middlewares/error.middleware'; +import { prisma } from './setup'; +import { createTestUser, createTestTask, generateAuthToken, createAuthHeaders } from './helpers'; + +const app = express(); +app.use(express.json()); +app.use('/api/dependencies', dependencyRoutes); +app.use(errorHandler); + +describe('Task Dependencies', () => { + let user: any; + let token: string; + + beforeEach(async () => { + user = await createTestUser(); + token = generateAuthToken(user.id); + }); + + describe('POST /api/dependencies', () => { + it('should create task dependency successfully', async () => { + const task1 = await createTestTask(user.id, { title: 'Task 1' }); + const task2 = await createTestTask(user.id, { title: 'Task 2' }); + + const response = await request(app) + .post('/api/dependencies') + .set(createAuthHeaders(token)) + .send({ + taskId: task2.id, + dependsOnTaskId: task1.id, + }) + .expect(201); + + expect(response.body).toMatchObject({ + success: true, + data: { + taskId: task2.id, + dependsOnTaskId: task1.id, + }, + }); + + // Verify dependency in database + const dependency = await prisma.taskDependency.findFirst({ + where: { taskId: task2.id, dependsOnTaskId: task1.id }, + }); + + expect(dependency).toBeTruthy(); + }); + + it('should prevent circular dependencies', async () => { + const task1 = await createTestTask(user.id, { title: 'Task 1' }); + const task2 = await createTestTask(user.id, { title: 'Task 2' }); + const task3 = await createTestTask(user.id, { title: 'Task 3' }); + + // Create dependency chain: task1 -> task2 -> task3 + await prisma.taskDependency.create({ + data: { taskId: task2.id, dependsOnTaskId: task1.id }, + }); + await prisma.taskDependency.create({ + data: { taskId: task3.id, dependsOnTaskId: task2.id }, + }); + + // Try to create circular dependency: task1 -> task3 (would create cycle) + const response = await request(app) + .post('/api/dependencies') + .set(createAuthHeaders(token)) + .send({ + taskId: task1.id, + dependsOnTaskId: task3.id, + }) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('circular dependency'); + }); + + it('should prevent self-dependency', async () => { + const task = await createTestTask(user.id); + + const response = await request(app) + .post('/api/dependencies') + .set(createAuthHeaders(token)) + .send({ + taskId: task.id, + dependsOnTaskId: task.id, + }) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('cannot depend on itself'); + }); + + it('should prevent duplicate dependencies', async () => { + const task1 = await createTestTask(user.id, { title: 'Task 1' }); + const task2 = await createTestTask(user.id, { title: 'Task 2' }); + + // Create dependency first time + await request(app) + .post('/api/dependencies') + .set(createAuthHeaders(token)) + .send({ + taskId: task2.id, + dependsOnTaskId: task1.id, + }) + .expect(201); + + // Try to create same dependency again + const response = await request(app) + .post('/api/dependencies') + .set(createAuthHeaders(token)) + .send({ + taskId: task2.id, + dependsOnTaskId: task1.id, + }) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('already exists'); + }); + + it('should validate that both tasks exist', async () => { + const task = await createTestTask(user.id); + + const response = await request(app) + .post('/api/dependencies') + .set(createAuthHeaders(token)) + .send({ + taskId: task.id, + dependsOnTaskId: 'non-existent-task-id', + }) + .expect(404); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('not found'); + }); + + it('should require authentication', async () => { + const task1 = await createTestTask(user.id); + const task2 = await createTestTask(user.id); + + const response = await request(app) + .post('/api/dependencies') + .send({ + taskId: task2.id, + dependsOnTaskId: task1.id, + }) + .expect(401); + + expect(response.body.success).toBe(false); + }); + }); + + describe('GET /api/dependencies/task/:taskId', () => { + it('should get task dependencies successfully', async () => { + const task1 = await createTestTask(user.id, { title: 'Dependency 1' }); + const task2 = await createTestTask(user.id, { title: 'Dependency 2' }); + const mainTask = await createTestTask(user.id, { title: 'Main Task' }); + + // Create dependencies + await prisma.taskDependency.create({ + data: { taskId: mainTask.id, dependsOnTaskId: task1.id }, + }); + await prisma.taskDependency.create({ + data: { taskId: mainTask.id, dependsOnTaskId: task2.id }, + }); + + const response = await request(app) + .get(`/api/dependencies/task/${mainTask.id}`) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body).toMatchObject({ + success: true, + data: { + dependencies: expect.any(Array), + dependents: expect.any(Array), + }, + }); + + expect(response.body.data.dependencies).toHaveLength(2); + expect(response.body.data.dependencies.map((d: any) => d.dependsOnTask.title)) + .toContain('Dependency 1'); + expect(response.body.data.dependencies.map((d: any) => d.dependsOnTask.title)) + .toContain('Dependency 2'); + }); + + it('should get task dependents successfully', async () => { + const baseTask = await createTestTask(user.id, { title: 'Base Task' }); + const dependent1 = await createTestTask(user.id, { title: 'Dependent 1' }); + const dependent2 = await createTestTask(user.id, { title: 'Dependent 2' }); + + // Create dependencies + await prisma.taskDependency.create({ + data: { taskId: dependent1.id, dependsOnTaskId: baseTask.id }, + }); + await prisma.taskDependency.create({ + data: { taskId: dependent2.id, dependsOnTaskId: baseTask.id }, + }); + + const response = await request(app) + .get(`/api/dependencies/task/${baseTask.id}`) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.data.dependents).toHaveLength(2); + expect(response.body.data.dependents.map((d: any) => d.task.title)) + .toContain('Dependent 1'); + expect(response.body.data.dependents.map((d: any) => d.task.title)) + .toContain('Dependent 2'); + }); + + it('should return empty arrays for task with no dependencies', async () => { + const task = await createTestTask(user.id); + + const response = await request(app) + .get(`/api/dependencies/task/${task.id}`) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body).toMatchObject({ + success: true, + data: { + dependencies: [], + dependents: [], + }, + }); + }); + }); + + describe('DELETE /api/dependencies/:id', () => { + it('should delete dependency successfully', async () => { + const task1 = await createTestTask(user.id); + const task2 = await createTestTask(user.id); + + const dependency = await prisma.taskDependency.create({ + data: { taskId: task2.id, dependsOnTaskId: task1.id }, + }); + + const response = await request(app) + .delete(`/api/dependencies/${dependency.id}`) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body).toMatchObject({ + success: true, + message: 'Dependency deleted successfully', + }); + + // Verify deletion in database + const deletedDependency = await prisma.taskDependency.findUnique({ + where: { id: dependency.id }, + }); + + expect(deletedDependency).toBeNull(); + }); + + it('should return 404 for non-existent dependency', async () => { + const response = await request(app) + .delete('/api/dependencies/non-existent-id') + .set(createAuthHeaders(token)) + .expect(404); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('not found'); + }); + + it('should require authentication', async () => { + const task1 = await createTestTask(user.id); + const task2 = await createTestTask(user.id); + + const dependency = await prisma.taskDependency.create({ + data: { taskId: task2.id, dependsOnTaskId: task1.id }, + }); + + const response = await request(app) + .delete(`/api/dependencies/${dependency.id}`) + .expect(401); + + expect(response.body.success).toBe(false); + }); + }); + + describe('GET /api/dependencies/validate-circular', () => { + it('should detect circular dependency correctly', async () => { + const task1 = await createTestTask(user.id, { title: 'Task 1' }); + const task2 = await createTestTask(user.id, { title: 'Task 2' }); + const task3 = await createTestTask(user.id, { title: 'Task 3' }); + + // Create dependency chain: task1 -> task2 -> task3 + await prisma.taskDependency.create({ + data: { taskId: task2.id, dependsOnTaskId: task1.id }, + }); + await prisma.taskDependency.create({ + data: { taskId: task3.id, dependsOnTaskId: task2.id }, + }); + + // Check if task1 -> task3 would create circular dependency + const response = await request(app) + .get('/api/dependencies/validate-circular') + .query({ + taskId: task1.id, + dependsOnTaskId: task3.id, + }) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body).toMatchObject({ + success: true, + data: { + wouldCreateCircular: true, + }, + }); + }); + + it('should confirm valid dependency', async () => { + const task1 = await createTestTask(user.id, { title: 'Task 1' }); + const task2 = await createTestTask(user.id, { title: 'Task 2' }); + + const response = await request(app) + .get('/api/dependencies/validate-circular') + .query({ + taskId: task2.id, + dependsOnTaskId: task1.id, + }) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body).toMatchObject({ + success: true, + data: { + wouldCreateCircular: false, + }, + }); + }); + }); +}); \ No newline at end of file diff --git a/tests/helpers.ts b/tests/helpers.ts new file mode 100755 index 0000000..e580591 Binary files /dev/null and b/tests/helpers.ts differ diff --git a/tests/setup.ts b/tests/setup.ts new file mode 100755 index 0000000..2a31bdc --- /dev/null +++ b/tests/setup.ts @@ -0,0 +1,85 @@ +import { PrismaClient } from '@prisma/client'; +import dotenv from 'dotenv'; + +// Load test environment variables +dotenv.config({ path: '.env.test' }); + +// Create Prisma client with test database URL +const prisma = new PrismaClient({ + datasources: { + db: { + url: process.env.TEST_DATABASE_URL || process.env.DATABASE_URL, + }, + }, + log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], +}); + +beforeAll(async () => { + try { + // Connect to test database + await prisma.$connect(); + + // Verify database connection + await prisma.$queryRaw`SELECT 1`; + + console.log('✅ Test database connected successfully'); + } catch (error) { + console.error('❌ Failed to connect to test database:', error); + throw error; + } +}); + +afterAll(async () => { + try { + // Clean up test data and disconnect + await cleanupDatabase(); + await prisma.$disconnect(); + console.log('✅ Test database disconnected successfully'); + } catch (error) { + console.error('❌ Error during test cleanup:', error); + throw error; + } +}); + +beforeEach(async () => { + // Clean up test data before each test + await cleanupDatabase(); +}); + +afterEach(async () => { + // Additional cleanup after each test if needed + // This can be useful for tests that might leave connections open +}); + +async function cleanupDatabase() { + try { + // Delete in reverse order of dependencies to avoid foreign key constraints + await prisma.activity.deleteMany({}); + await prisma.comment.deleteMany({}); + await prisma.taskDependency.deleteMany({}); + await prisma.taskAssignment.deleteMany({}); + await prisma.task.deleteMany({}); + await prisma.teamMember.deleteMany({}); + await prisma.team.deleteMany({}); + await prisma.user.deleteMany({}); + } catch (error) { + console.error('❌ Error during database cleanup:', error); + throw error; + } +} + +// Global test timeout for database operations +jest.setTimeout(30000); + +// Mock console methods in test environment to reduce noise +if (process.env.NODE_ENV === 'test') { + global.console = { + ...console, + log: jest.fn(), + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + }; +} + +export { prisma }; diff --git a/tests/task.test.ts b/tests/task.test.ts new file mode 100755 index 0000000..96bdc4b --- /dev/null +++ b/tests/task.test.ts @@ -0,0 +1,680 @@ +import { TaskPriority, TaskStatus, TeamRole } from '@prisma/client'; +import express from 'express'; +import request from 'supertest'; +import { errorHandler } from '../src/middlewares/error.middleware'; +import taskRoutes from '../src/routes/task.routes'; +import { createAuthHeaders, createTestTask, createTestTeam, createTestUser, generateAuthToken } from './helpers'; +import { prisma } from './setup'; + +const app = express(); +app.use(express.json()); +app.use('/api/tasks', taskRoutes); +app.use(errorHandler); + +describe('Task Management', () => { + let user: any; + let token: string; + let team: any; + + beforeEach(async () => { + user = await createTestUser(); + token = generateAuthToken(user.id); + team = await createTestTeam(user.id); + }); + + describe('POST /api/tasks', () => { + it('should create a new task successfully', async () => { + const taskData = { + title: 'Test Task', + description: 'Test task description', + priority: TaskPriority.HIGH, + dueDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), + teamId: team.id, + assigneeIds: [user.id], + }; + + const response = await request(app) + .post('/api/tasks') + .set(createAuthHeaders(token)) + .send(taskData) + .expect(201); + + expect(response.body).toMatchObject({ + success: true, + data: { + title: taskData.title, + description: taskData.description, + priority: taskData.priority, + status: TaskStatus.TODO, + createdById: user.id, + teamId: team.id, + }, + }); + + expect(response.body.data.id).toBeDefined(); + expect(response.body.data.assignments).toHaveLength(1); + }); + + it('should validate required fields', async () => { + const invalidTaskData = { + description: 'Missing title', + }; + + const response = await request(app) + .post('/api/tasks') + .set(createAuthHeaders(token)) + .send(invalidTaskData) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('validation'); + }); + + it('should require authentication', async () => { + const taskData = { + title: 'Test Task', + description: 'Test task description', + }; + + const response = await request(app) + .post('/api/tasks') + .send(taskData) + .expect(401); + + expect(response.body.success).toBe(false); + }); + }); + + describe('GET /api/tasks', () => { + it('should get user tasks with default pagination', async () => { + // Create test tasks + await createTestTask(user.id, { title: 'Task 1' }); + await createTestTask(user.id, { title: 'Task 2' }); + + const response = await request(app) + .get('/api/tasks') + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body).toMatchObject({ + success: true, + data: { + tasks: expect.any(Array), + pagination: { + page: 1, + limit: 10, + total: 2, + totalPages: 1, + }, + }, + }); + + expect(response.body.data.tasks).toHaveLength(2); + }); + + it('should filter tasks by status', async () => { + await createTestTask(user.id, { title: 'Todo Task', status: TaskStatus.TODO }); + await createTestTask(user.id, { title: 'In Progress Task', status: TaskStatus.IN_PROGRESS }); + + const response = await request(app) + .get('/api/tasks?status=TODO') + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.data.tasks).toHaveLength(1); + expect(response.body.data.tasks[0].status).toBe(TaskStatus.TODO); + }); + + it('should filter tasks by priority', async () => { + await createTestTask(user.id, { title: 'High Priority', priority: TaskPriority.HIGH }); + await createTestTask(user.id, { title: 'Low Priority', priority: TaskPriority.LOW }); + + const response = await request(app) + .get('/api/tasks?priority=HIGH') + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.data.tasks).toHaveLength(1); + expect(response.body.data.tasks[0].priority).toBe(TaskPriority.HIGH); + }); + + it('should search tasks by title', async () => { + await createTestTask(user.id, { title: 'Important Task' }); + await createTestTask(user.id, { title: 'Regular Task' }); + + const response = await request(app) + .get('/api/tasks?search=Important') + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.data.tasks).toHaveLength(1); + expect(response.body.data.tasks[0].title).toContain('Important'); + }); + }); + + describe('GET /api/tasks/:id', () => { + it('should get task by id', async () => { + const task = await createTestTask(user.id); + + const response = await request(app) + .get(`/api/tasks/${task.id}`) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body).toMatchObject({ + success: true, + data: { + id: task.id, + title: task.title, + description: task.description, + }, + }); + }); + + it('should return 404 for non-existent task', async () => { + const response = await request(app) + .get('/api/tasks/non-existent-id') + .set(createAuthHeaders(token)) + .expect(404); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('not found'); + }); + }); + + describe('PUT /api/tasks/:id', () => { + it('should update task successfully', async () => { + const task = await createTestTask(user.id); + const updateData = { + title: 'Updated Task Title', + status: TaskStatus.IN_PROGRESS, + priority: TaskPriority.HIGH, + }; + + const response = await request(app) + .put(`/api/tasks/${task.id}`) + .set(createAuthHeaders(token)) + .send(updateData) + .expect(200); + + expect(response.body).toMatchObject({ + success: true, + data: { + id: task.id, + ...updateData, + }, + }); + + // Verify update in database + const updatedTask = await prisma.task.findUnique({ + where: { id: task.id }, + }); + + expect(updatedTask).toMatchObject(updateData); + }); + + it('should validate update data', async () => { + const task = await createTestTask(user.id); + const invalidData = { + status: 'INVALID_STATUS', + priority: 'INVALID_PRIORITY', + }; + + const response = await request(app) + .put(`/api/tasks/${task.id}`) + .set(createAuthHeaders(token)) + .send(invalidData) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('validation'); + }); + }); + + describe('DELETE /api/tasks/:id', () => { + it('should delete task successfully', async () => { + const task = await createTestTask(user.id); + + const response = await request(app) + .delete(`/api/tasks/${task.id}`) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body).toMatchObject({ + success: true, + message: 'Task deleted successfully', + }); + + // Verify deletion in database + const deletedTask = await prisma.task.findUnique({ + where: { id: task.id }, + }); + + expect(deletedTask).toBeNull(); + }); + + it('should return 404 for non-existent task', async () => { + const response = await request(app) + .delete('/api/tasks/non-existent-id') + .set(createAuthHeaders(token)) + .expect(404); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('not found'); + }); + }); + + describe('POST /api/tasks/:id/assign', () => { + it('should assign task to users', async () => { + const task = await createTestTask(user.id); + const assignee = await createTestUser({ email: 'assignee@example.com' }); + + const response = await request(app) + .post(`/api/tasks/${task.id}/assign`) + .set(createAuthHeaders(token)) + .send({ userIds: [assignee.id] }) + .expect(200); + + expect(response.body.success).toBe(true); + + // Verify assignment in database + const assignments = await prisma.taskAssignment.findMany({ + where: { taskId: task.id }, + }); + + expect(assignments).toHaveLength(2); // Original assignee + new assignee + }); + }); + + describe('DELETE /api/tasks/:id/assign/:userId', () => { + it('should unassign user from task', async () => { + const task = await createTestTask(user.id); + + const response = await request(app) + .delete(`/api/tasks/${task.id}/assign/${user.id}`) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.success).toBe(true); + + // Verify unassignment in database + const assignment = await prisma.taskAssignment.findFirst({ + where: { taskId: task.id, userId: user.id }, + }); + + expect(assignment).toBeNull(); + }); + }); + + describe('Task Status Management', () => { + it('should update task status', async () => { + const task = await createTestTask(user.id); + + const response = await request(app) + .put(`/api/tasks/${task.id}/status`) + .set(createAuthHeaders(token)) + .send({ status: TaskStatus.IN_PROGRESS }) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data.status).toBe(TaskStatus.IN_PROGRESS); + + // Verify status update in database + const updatedTask = await prisma.task.findUnique({ + where: { id: task.id }, + }); + + expect(updatedTask?.status).toBe(TaskStatus.IN_PROGRESS); + }); + + it('should track completion time when status changes to DONE', async () => { + const task = await createTestTask(user.id); + + const response = await request(app) + .put(`/api/tasks/${task.id}/status`) + .set(createAuthHeaders(token)) + .send({ status: TaskStatus.DONE }) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data.completedAt).toBeDefined(); + + // Verify completion time in database + const updatedTask = await prisma.task.findUnique({ + where: { id: task.id }, + }); + + expect(updatedTask?.completedAt).toBeDefined(); + }); + + it('should validate status transitions', async () => { + const task = await createTestTask(user.id); + + const response = await request(app) + .put(`/api/tasks/${task.id}/status`) + .set(createAuthHeaders(token)) + .send({ status: 'INVALID_STATUS' }) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('validation'); + }); + }); + + describe('Task Priority Management', () => { + it('should update task priority', async () => { + const task = await createTestTask(user.id, { priority: TaskPriority.LOW }); + + const response = await request(app) + .put(`/api/tasks/${task.id}/priority`) + .set(createAuthHeaders(token)) + .send({ priority: TaskPriority.URGENT }) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data.priority).toBe(TaskPriority.URGENT); + }); + + it('should validate priority values', async () => { + const task = await createTestTask(user.id); + + const response = await request(app) + .put(`/api/tasks/${task.id}/priority`) + .set(createAuthHeaders(token)) + .send({ priority: 'INVALID_PRIORITY' }) + .expect(400); + + expect(response.body.success).toBe(false); + }); + }); + + describe('Task Filtering and Searching', () => { + beforeEach(async () => { + // Create tasks with different attributes for filtering + await createTestTask(user.id, { + title: 'High Priority Task', + priority: TaskPriority.HIGH, + status: TaskStatus.TODO + }); + await createTestTask(user.id, { + title: 'Urgent Bug Fix', + priority: TaskPriority.URGENT, + status: TaskStatus.IN_PROGRESS + }); + await createTestTask(user.id, { + title: 'Low Priority Feature', + priority: TaskPriority.LOW, + status: TaskStatus.DONE + }); + }); + + it('should filter tasks by status', async () => { + const response = await request(app) + .get('/api/tasks') + .query({ status: TaskStatus.IN_PROGRESS }) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data.length).toBe(1); + expect(response.body.data[0].title).toBe('Urgent Bug Fix'); + }); + + it('should filter tasks by priority', async () => { + const response = await request(app) + .get('/api/tasks') + .query({ priority: TaskPriority.URGENT }) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data.length).toBe(1); + expect(response.body.data[0].priority).toBe(TaskPriority.URGENT); + }); + + it('should search tasks by title', async () => { + const response = await request(app) + .get('/api/tasks/search') + .query({ q: 'Bug' }) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data.length).toBe(1); + expect(response.body.data[0].title).toContain('Bug'); + }); + + it('should filter tasks by due date range', async () => { + const futureDate = new Date(Date.now() + 10 * 24 * 60 * 60 * 1000); + await createTestTask(user.id, { + title: 'Future Task', + dueDate: futureDate + }); + + const response = await request(app) + .get('/api/tasks') + .query({ + dueBefore: futureDate.toISOString(), + dueAfter: new Date().toISOString() + }) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data.some((task: any) => task.title === 'Future Task')).toBe(true); + }); + }); + + describe('Task Sorting', () => { + beforeEach(async () => { + // Create tasks with different creation times and priorities + await createTestTask(user.id, { + title: 'First Task', + priority: TaskPriority.LOW + }); + await new Promise(resolve => setTimeout(resolve, 10)); // Small delay + await createTestTask(user.id, { + title: 'Second Task', + priority: TaskPriority.HIGH + }); + }); + + it('should sort tasks by creation date', async () => { + const response = await request(app) + .get('/api/tasks') + .query({ sortBy: 'createdAt', sortOrder: 'desc' }) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data[0].title).toBe('Second Task'); + }); + + it('should sort tasks by priority', async () => { + const response = await request(app) + .get('/api/tasks') + .query({ sortBy: 'priority', sortOrder: 'desc' }) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data[0].priority).toBe(TaskPriority.HIGH); + }); + }); + + describe('Team Task Management', () => { + let teamMember: any; + let otherTeam: any; + + beforeEach(async () => { + teamMember = await createTestUser(); + await prisma.teamMember.create({ + data: { + userId: teamMember.id, + teamId: team.id, + role: TeamRole.MEMBER, + }, + }); + + // Create another team for permission testing + otherTeam = await createTestTeam(teamMember.id); + }); + + it('should allow team members to view team tasks', async () => { + const teamTask = await createTestTask(user.id, { teamId: team.id }); + const memberToken = generateAuthToken(teamMember.id); + + const response = await request(app) + .get(`/api/tasks/${teamTask.id}`) + .set(createAuthHeaders(memberToken)) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data.id).toBe(teamTask.id); + }); + + it('should prevent non-team members from viewing team tasks', async () => { + const teamTask = await createTestTask(user.id, { teamId: team.id }); + const outsider = await createTestUser(); + const outsiderToken = generateAuthToken(outsider.id); + + const response = await request(app) + .get(`/api/tasks/${teamTask.id}`) + .set(createAuthHeaders(outsiderToken)) + .expect(403); + + expect(response.body.success).toBe(false); + }); + + it('should allow task creation within team', async () => { + const memberToken = generateAuthToken(teamMember.id); + + const taskData = { + title: 'Team Member Task', + description: 'Task created by team member', + teamId: team.id, + assigneeIds: [teamMember.id], + }; + + const response = await request(app) + .post('/api/tasks') + .set(createAuthHeaders(memberToken)) + .send(taskData) + .expect(201); + + expect(response.body.success).toBe(true); + expect(response.body.data.teamId).toBe(team.id); + }); + }); + + describe('Task Validation and Error Handling', () => { + it('should validate required fields', async () => { + const invalidTaskData = { + description: 'Missing title', + }; + + const response = await request(app) + .post('/api/tasks') + .set(createAuthHeaders(token)) + .send(invalidTaskData) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('validation'); + }); + + it('should validate due date format', async () => { + const taskData = { + title: 'Test Task', + dueDate: 'invalid-date-format', + }; + + const response = await request(app) + .post('/api/tasks') + .set(createAuthHeaders(token)) + .send(taskData) + .expect(400); + + expect(response.body.success).toBe(false); + }); + + it('should prevent assigning to non-existent users', async () => { + const taskData = { + title: 'Test Task', + assigneeIds: ['non-existent-user-id'], + }; + + const response = await request(app) + .post('/api/tasks') + .set(createAuthHeaders(token)) + .send(taskData) + .expect(400); + + expect(response.body.success).toBe(false); + }); + + it('should handle very long task titles and descriptions', async () => { + const longTitle = 'a'.repeat(1000); + const longDescription = 'b'.repeat(5000); + + const taskData = { + title: longTitle, + description: longDescription, + }; + + const response = await request(app) + .post('/api/tasks') + .set(createAuthHeaders(token)) + .send(taskData) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('validation'); + }); + }); + + describe('Task Analytics and Reporting', () => { + beforeEach(async () => { + // Create tasks with different statuses for analytics + await createTestTask(user.id, { status: TaskStatus.TODO }); + await createTestTask(user.id, { status: TaskStatus.IN_PROGRESS }); + await createTestTask(user.id, { status: TaskStatus.DONE }); + await createTestTask(user.id, { status: TaskStatus.DONE }); + }); + + it('should get task statistics', async () => { + const response = await request(app) + .get('/api/tasks/stats') + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data).toMatchObject({ + total: 4, + byStatus: { + [TaskStatus.TODO]: 1, + [TaskStatus.IN_PROGRESS]: 1, + [TaskStatus.DONE]: 2, + }, + }); + }); + + it('should get overdue tasks', async () => { + const pastDate = new Date(Date.now() - 24 * 60 * 60 * 1000); // Yesterday + await createTestTask(user.id, { + title: 'Overdue Task', + dueDate: pastDate, + status: TaskStatus.TODO + }); + + const response = await request(app) + .get('/api/tasks/overdue') + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data.length).toBe(1); + expect(response.body.data[0].title).toBe('Overdue Task'); + }); + }); +}); \ No newline at end of file diff --git a/tests/team.test.ts b/tests/team.test.ts new file mode 100755 index 0000000..bc03b00 --- /dev/null +++ b/tests/team.test.ts @@ -0,0 +1,800 @@ +import { TeamRole } from '@prisma/client'; +import express from 'express'; +import request from 'supertest'; +import { errorHandler } from '../src/middlewares/error.middleware'; +import teamRoutes from '../src/routes/team.routes'; +import { createAuthHeaders, createTestTeam, createTestUser, generateAuthToken } from './helpers'; +import { prisma } from './setup'; + +const app = express(); +app.use(express.json()); +app.use('/api/teams', teamRoutes); +app.use(errorHandler); + +describe('Team Management', () => { + let user: any; + let token: string; + + beforeEach(async () => { + user = await createTestUser(); + token = generateAuthToken(user.id); + }); + + describe('POST /api/teams', () => { + it('should create a new team successfully', async () => { + const teamData = { + name: 'Test Team', + description: 'Test team description', + }; + + const response = await request(app) + .post('/api/teams') + .set(createAuthHeaders(token)) + .send(teamData) + .expect(201); + + expect(response.body).toMatchObject({ + success: true, + data: { + name: teamData.name, + description: teamData.description, + }, + }); + + expect(response.body.data.id).toBeDefined(); + + // Verify creator is team owner + const membership = await prisma.teamMember.findFirst({ + where: { teamId: response.body.data.id, userId: user.id }, + }); + + expect(membership?.role).toBe(TeamRole.OWNER); + }); + + it('should validate required fields', async () => { + const invalidTeamData = { + description: 'Missing name', + }; + + const response = await request(app) + .post('/api/teams') + .set(createAuthHeaders(token)) + .send(invalidTeamData) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('validation'); + }); + + it('should require authentication', async () => { + const teamData = { + name: 'Test Team', + description: 'Test team description', + }; + + const response = await request(app) + .post('/api/teams') + .send(teamData) + .expect(401); + + expect(response.body.success).toBe(false); + }); + }); + + describe('GET /api/teams', () => { + it('should get user teams', async () => { + const team1 = await createTestTeam(user.id, { name: 'Team 1' }); + const team2 = await createTestTeam(user.id, { name: 'Team 2' }); + + const response = await request(app) + .get('/api/teams') + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body).toMatchObject({ + success: true, + data: expect.any(Array), + }); + + expect(response.body.data).toHaveLength(2); + expect(response.body.data.map((t: any) => t.name)).toContain('Team 1'); + expect(response.body.data.map((t: any) => t.name)).toContain('Team 2'); + }); + + it('should return empty array for user with no teams', async () => { + const response = await request(app) + .get('/api/teams') + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body).toMatchObject({ + success: true, + data: [], + }); + }); + }); + + describe('GET /api/teams/:id', () => { + it('should get team by id with members', async () => { + const team = await createTestTeam(user.id); + + const response = await request(app) + .get(`/api/teams/${team.id}`) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body).toMatchObject({ + success: true, + data: { + id: team.id, + name: team.name, + description: team.description, + members: expect.any(Array), + }, + }); + + expect(response.body.data.members).toHaveLength(1); + expect(response.body.data.members[0].role).toBe(TeamRole.OWNER); + }); + + it('should return 404 for non-existent team', async () => { + const response = await request(app) + .get('/api/teams/non-existent-id') + .set(createAuthHeaders(token)) + .expect(404); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('not found'); + }); + + it('should deny access to non-member', async () => { + const otherUser = await createTestUser({ email: 'other@example.com' }); + const team = await createTestTeam(otherUser.id); + + const response = await request(app) + .get(`/api/teams/${team.id}`) + .set(createAuthHeaders(token)) + .expect(403); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('not a member'); + }); + }); + + describe('PUT /api/teams/:id', () => { + it('should update team successfully', async () => { + const team = await createTestTeam(user.id); + const updateData = { + name: 'Updated Team Name', + description: 'Updated description', + }; + + const response = await request(app) + .put(`/api/teams/${team.id}`) + .set(createAuthHeaders(token)) + .send(updateData) + .expect(200); + + expect(response.body).toMatchObject({ + success: true, + data: { + id: team.id, + ...updateData, + }, + }); + + // Verify update in database + const updatedTeam = await prisma.team.findUnique({ + where: { id: team.id }, + }); + + expect(updatedTeam).toMatchObject(updateData); + }); + + it('should deny access to non-owner', async () => { + const owner = await createTestUser({ email: 'owner@example.com' }); + const team = await createTestTeam(owner.id); + + // Add current user as member (not owner) + await prisma.teamMember.create({ + data: { + userId: user.id, + teamId: team.id, + role: TeamRole.MEMBER, + }, + }); + + const updateData = { + name: 'Updated Team Name', + }; + + const response = await request(app) + .put(`/api/teams/${team.id}`) + .set(createAuthHeaders(token)) + .send(updateData) + .expect(403); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('permission'); + }); + }); + + describe('DELETE /api/teams/:id', () => { + it('should delete team successfully', async () => { + const team = await createTestTeam(user.id); + + const response = await request(app) + .delete(`/api/teams/${team.id}`) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body).toMatchObject({ + success: true, + message: 'Team deleted successfully', + }); + + // Verify deletion in database + const deletedTeam = await prisma.team.findUnique({ + where: { id: team.id }, + }); + + expect(deletedTeam).toBeNull(); + }); + + it('should deny access to non-owner', async () => { + const owner = await createTestUser({ email: 'owner@example.com' }); + const team = await createTestTeam(owner.id); + + // Add current user as member (not owner) + await prisma.teamMember.create({ + data: { + userId: user.id, + teamId: team.id, + role: TeamRole.MEMBER, + }, + }); + + const response = await request(app) + .delete(`/api/teams/${team.id}`) + .set(createAuthHeaders(token)) + .expect(403); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('permission'); + }); + }); + + describe('POST /api/teams/:id/members', () => { + it('should add member to team successfully', async () => { + const team = await createTestTeam(user.id); + const newMember = await createTestUser({ email: 'member@example.com' }); + + const response = await request(app) + .post(`/api/teams/${team.id}/members`) + .set(createAuthHeaders(token)) + .send({ userId: newMember.id, role: TeamRole.MEMBER }) + .expect(200); + + expect(response.body.success).toBe(true); + + // Verify membership in database + const membership = await prisma.teamMember.findFirst({ + where: { teamId: team.id, userId: newMember.id }, + }); + + expect(membership).toBeTruthy(); + expect(membership?.role).toBe(TeamRole.MEMBER); + }); + + it('should prevent duplicate membership', async () => { + const team = await createTestTeam(user.id); + + const response = await request(app) + .post(`/api/teams/${team.id}/members`) + .set(createAuthHeaders(token)) + .send({ userId: user.id, role: TeamRole.MEMBER }) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('already a member'); + }); + + it('should deny access to non-admin', async () => { + const owner = await createTestUser({ email: 'owner@example.com' }); + const team = await createTestTeam(owner.id); + const newMember = await createTestUser({ email: 'member@example.com' }); + + // Add current user as member (not admin) + await prisma.teamMember.create({ + data: { + userId: user.id, + teamId: team.id, + role: TeamRole.MEMBER, + }, + }); + + const response = await request(app) + .post(`/api/teams/${team.id}/members`) + .set(createAuthHeaders(token)) + .send({ userId: newMember.id, role: TeamRole.MEMBER }) + .expect(403); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('permission'); + }); + }); + + describe('PUT /api/teams/:id/members/:userId', () => { + it('should update member role successfully', async () => { + const team = await createTestTeam(user.id); + const member = await createTestUser({ email: 'member@example.com' }); + + // Add member to team + await prisma.teamMember.create({ + data: { + userId: member.id, + teamId: team.id, + role: TeamRole.MEMBER, + }, + }); + + const response = await request(app) + .put(`/api/teams/${team.id}/members/${member.id}`) + .set(createAuthHeaders(token)) + .send({ role: TeamRole.ADMIN }) + .expect(200); + + expect(response.body.success).toBe(true); + + // Verify role update in database + const membership = await prisma.teamMember.findFirst({ + where: { teamId: team.id, userId: member.id }, + }); + + expect(membership?.role).toBe(TeamRole.ADMIN); + }); + }); + + describe('DELETE /api/teams/:id/members/:userId', () => { + it('should remove member from team successfully', async () => { + const team = await createTestTeam(user.id); + const member = await createTestUser({ email: 'member@example.com' }); + + // Add member to team + await prisma.teamMember.create({ + data: { + userId: member.id, + teamId: team.id, + role: TeamRole.MEMBER, + }, + }); + + const response = await request(app) + .delete(`/api/teams/${team.id}/members/${member.id}`) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.success).toBe(true); + + // Verify removal in database + const membership = await prisma.teamMember.findFirst({ + where: { teamId: team.id, userId: member.id }, + }); + + expect(membership).toBeNull(); + }); + + it('should prevent owner from removing themselves', async () => { + const team = await createTestTeam(user.id); + + const response = await request(app) + .delete(`/api/teams/${team.id}/members/${user.id}`) + .set(createAuthHeaders(token)) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('cannot remove owner'); + }); + }); + + describe('Team Permissions and Authorization', () => { + let team: any; + let admin: any; + let member: any; + let outsider: any; + let adminToken: string; + let memberToken: string; + let outsiderToken: string; + + beforeEach(async () => { + team = await createTestTeam(user.id); + admin = await createTestUser({ email: 'admin@example.com' }); + member = await createTestUser({ email: 'member@example.com' }); + outsider = await createTestUser({ email: 'outsider@example.com' }); + + adminToken = generateAuthToken(admin.id); + memberToken = generateAuthToken(member.id); + outsiderToken = generateAuthToken(outsider.id); + + // Add admin and member to team + await prisma.teamMember.create({ + data: { userId: admin.id, teamId: team.id, role: TeamRole.ADMIN }, + }); + await prisma.teamMember.create({ + data: { userId: member.id, teamId: team.id, role: TeamRole.MEMBER }, + }); + }); + + it('should allow owners to add members', async () => { + const newMember = await createTestUser({ email: 'newmember@example.com' }); + + const response = await request(app) + .post(`/api/teams/${team.id}/members`) + .set(createAuthHeaders(token)) + .send({ userId: newMember.id, role: TeamRole.MEMBER }) + .expect(200); + + expect(response.body.success).toBe(true); + }); + + it('should allow admins to add members', async () => { + const newMember = await createTestUser({ email: 'newmember2@example.com' }); + + const response = await request(app) + .post(`/api/teams/${team.id}/members`) + .set(createAuthHeaders(adminToken)) + .send({ userId: newMember.id, role: TeamRole.MEMBER }) + .expect(200); + + expect(response.body.success).toBe(true); + }); + + it('should prevent members from adding other members', async () => { + const newMember = await createTestUser({ email: 'newmember3@example.com' }); + + const response = await request(app) + .post(`/api/teams/${team.id}/members`) + .set(createAuthHeaders(memberToken)) + .send({ userId: newMember.id, role: TeamRole.MEMBER }) + .expect(403); + + expect(response.body.success).toBe(false); + }); + + it('should prevent outsiders from accessing team', async () => { + const response = await request(app) + .get(`/api/teams/${team.id}`) + .set(createAuthHeaders(outsiderToken)) + .expect(403); + + expect(response.body.success).toBe(false); + }); + + it('should allow members to view team details', async () => { + const response = await request(app) + .get(`/api/teams/${team.id}`) + .set(createAuthHeaders(memberToken)) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data.id).toBe(team.id); + }); + + it('should prevent members from updating team settings', async () => { + const response = await request(app) + .put(`/api/teams/${team.id}`) + .set(createAuthHeaders(memberToken)) + .send({ name: 'Updated Team Name' }) + .expect(403); + + expect(response.body.success).toBe(false); + }); + + it('should allow admins to update team settings', async () => { + const response = await request(app) + .put(`/api/teams/${team.id}`) + .set(createAuthHeaders(adminToken)) + .send({ name: 'Updated by Admin' }) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data.name).toBe('Updated by Admin'); + }); + }); + + describe('Team Role Management', () => { + let team: any; + let member: any; + + beforeEach(async () => { + team = await createTestTeam(user.id); + member = await createTestUser({ email: 'roletest@example.com' }); + + await prisma.teamMember.create({ + data: { userId: member.id, teamId: team.id, role: TeamRole.MEMBER }, + }); + }); + + it('should promote member to admin', async () => { + const response = await request(app) + .put(`/api/teams/${team.id}/members/${member.id}/role`) + .set(createAuthHeaders(token)) + .send({ role: TeamRole.ADMIN }) + .expect(200); + + expect(response.body.success).toBe(true); + + const membership = await prisma.teamMember.findFirst({ + where: { teamId: team.id, userId: member.id }, + }); + + expect(membership?.role).toBe(TeamRole.ADMIN); + }); + + it('should demote admin to member', async () => { + // First promote to admin + await prisma.teamMember.update({ + where: { userId_teamId: { userId: member.id, teamId: team.id } }, + data: { role: TeamRole.ADMIN }, + }); + + const response = await request(app) + .put(`/api/teams/${team.id}/members/${member.id}/role`) + .set(createAuthHeaders(token)) + .send({ role: TeamRole.MEMBER }) + .expect(200); + + expect(response.body.success).toBe(true); + + const membership = await prisma.teamMember.findFirst({ + where: { teamId: team.id, userId: member.id }, + }); + + expect(membership?.role).toBe(TeamRole.MEMBER); + }); + + it('should prevent changing owner role', async () => { + const response = await request(app) + .put(`/api/teams/${team.id}/members/${user.id}/role`) + .set(createAuthHeaders(token)) + .send({ role: TeamRole.MEMBER }) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('owner'); + }); + + it('should validate role values', async () => { + const response = await request(app) + .put(`/api/teams/${team.id}/members/${member.id}/role`) + .set(createAuthHeaders(token)) + .send({ role: 'INVALID_ROLE' }) + .expect(400); + + expect(response.body.success).toBe(false); + }); + }); + + describe('Team Statistics and Analytics', () => { + let team: any; + + beforeEach(async () => { + team = await createTestTeam(user.id); + + // Add multiple members + for (let i = 0; i < 3; i++) { + const member = await createTestUser({ email: `member${i}@example.com` }); + await prisma.teamMember.create({ + data: { userId: member.id, teamId: team.id, role: TeamRole.MEMBER }, + }); + } + + // Add an admin + const admin = await createTestUser({ email: 'admin@example.com' }); + await prisma.teamMember.create({ + data: { userId: admin.id, teamId: team.id, role: TeamRole.ADMIN }, + }); + }); + + it('should get team statistics', async () => { + const response = await request(app) + .get(`/api/teams/${team.id}/stats`) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data).toMatchObject({ + memberCount: 5, // owner + 3 members + 1 admin + roleDistribution: { + [TeamRole.OWNER]: 1, + [TeamRole.ADMIN]: 1, + [TeamRole.MEMBER]: 3, + }, + }); + }); + + it('should get team activity summary', async () => { + // Create some team tasks first + await prisma.task.create({ + data: { + title: 'Team Task 1', + createdById: user.id, + teamId: team.id, + }, + }); + + const response = await request(app) + .get(`/api/teams/${team.id}/activity`) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data.taskCount).toBe(1); + }); + }); + + describe('Team Search and Discovery', () => { + beforeEach(async () => { + await createTestTeam(user.id, { + name: 'Engineering Team', + description: 'Software development team' + }); + await createTestTeam(user.id, { + name: 'Marketing Team', + description: 'Product marketing and promotion' + }); + await createTestTeam(user.id, { + name: 'Sales Engineering', + description: 'Technical sales support' + }); + }); + + it('should search teams by name', async () => { + const response = await request(app) + .get('/api/teams/search') + .query({ q: 'Engineering' }) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data.length).toBe(2); + expect(response.body.data.every((team: any) => + team.name.includes('Engineering') + )).toBe(true); + }); + + it('should search teams by description', async () => { + const response = await request(app) + .get('/api/teams/search') + .query({ q: 'marketing' }) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data.length).toBe(1); + expect(response.body.data[0].name).toBe('Marketing Team'); + }); + }); + + describe('Team Deletion and Cleanup', () => { + it('should delete team and clean up related data', async () => { + const team = await createTestTeam(user.id); + + // Create some team data + const task = await prisma.task.create({ + data: { + title: 'Team Task', + createdById: user.id, + teamId: team.id, + }, + }); + + const response = await request(app) + .delete(`/api/teams/${team.id}`) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.success).toBe(true); + + // Verify team is deleted + const deletedTeam = await prisma.team.findUnique({ + where: { id: team.id }, + }); + expect(deletedTeam).toBeNull(); + + // Verify team members are cleaned up + const members = await prisma.teamMember.findMany({ + where: { teamId: team.id }, + }); + expect(members).toHaveLength(0); + + // Verify tasks are handled appropriately (likely set teamId to null) + const orphanedTask = await prisma.task.findUnique({ + where: { id: task.id }, + }); + expect(orphanedTask?.teamId).toBeNull(); + }); + + it('should only allow owner to delete team', async () => { + const team = await createTestTeam(user.id); + const member = await createTestUser({ email: 'member@example.com' }); + const memberToken = generateAuthToken(member.id); + + await prisma.teamMember.create({ + data: { userId: member.id, teamId: team.id, role: TeamRole.ADMIN }, + }); + + const response = await request(app) + .delete(`/api/teams/${team.id}`) + .set(createAuthHeaders(memberToken)) + .expect(403); + + expect(response.body.success).toBe(false); + }); + }); + + describe('Edge Cases and Error Handling', () => { + it('should handle team name conflicts', async () => { + const teamName = 'Duplicate Team Name'; + + await createTestTeam(user.id, { name: teamName }); + + const response = await request(app) + .post('/api/teams') + .set(createAuthHeaders(token)) + .send({ name: teamName, description: 'Another team' }) + .expect(409); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('already exists'); + }); + + it('should handle adding non-existent user to team', async () => { + const team = await createTestTeam(user.id); + + const response = await request(app) + .post(`/api/teams/${team.id}/members`) + .set(createAuthHeaders(token)) + .send({ userId: 'non-existent-id', role: TeamRole.MEMBER }) + .expect(404); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('User not found'); + }); + + it('should handle adding already existing member', async () => { + const team = await createTestTeam(user.id); + const member = await createTestUser({ email: 'existing@example.com' }); + + // Add member first time + await request(app) + .post(`/api/teams/${team.id}/members`) + .set(createAuthHeaders(token)) + .send({ userId: member.id, role: TeamRole.MEMBER }) + .expect(200); + + // Try to add same member again + const response = await request(app) + .post(`/api/teams/${team.id}/members`) + .set(createAuthHeaders(token)) + .send({ userId: member.id, role: TeamRole.MEMBER }) + .expect(409); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('already a member'); + }); + + it('should validate team name length', async () => { + const longName = 'a'.repeat(256); + + const response = await request(app) + .post('/api/teams') + .set(createAuthHeaders(token)) + .send({ name: longName, description: 'Test' }) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('validation'); + }); + }); +}); \ No newline at end of file diff --git a/tests/user.service.test.ts b/tests/user.service.test.ts new file mode 100755 index 0000000..4ec58e0 --- /dev/null +++ b/tests/user.service.test.ts @@ -0,0 +1,250 @@ +import bcrypt from 'bcryptjs'; +import * as userService from '../src/services/user.service'; +import { createTestUser } from './helpers'; +import { prisma } from './setup'; + +describe('UserService', () => { + describe('register', () => { + it('should create a new user with hashed password', async () => { + const userData = { + email: 'test@example.com', + password: 'testPassword123', + firstName: 'John', + lastName: 'Doe', + }; + + const user = await userService.register(userData); + + expect(user).toBeDefined(); + expect(user.email).toBe(userData.email); + expect(user.firstName).toBe(userData.firstName); + expect(user.lastName).toBe(userData.lastName); + expect((user as any).password).toBeUndefined(); // Password should not be returned + + // Verify password is properly hashed in database + const userInDb = await prisma.user.findUnique({ where: { id: user.id } }); + expect(userInDb?.password).not.toBe(userData.password); + + const isPasswordValid = await bcrypt.compare(userData.password, userInDb!.password); + expect(isPasswordValid).toBe(true); + }); + + it('should throw error for duplicate email', async () => { + const userData = { + email: 'duplicate@example.com', + password: 'testPassword123', + firstName: 'John', + lastName: 'Doe', + }; + + await userService.register(userData); + + await expect(userService.register(userData)).rejects.toThrow(); + }); + + it('should trim and normalize email', async () => { + const userData = { + email: ' TEST@EXAMPLE.COM ', + password: 'testPassword123', + firstName: 'John', + lastName: 'Doe', + }; + + const user = await userService.register(userData); + expect(user.email).toBe('test@example.com'); + }); + }); + + describe('login', () => { + it('should authenticate user with valid credentials', async () => { + const user = await createTestUser(); + + const result = await userService.login({ + email: user.email, + password: 'testPassword123' + }); + + expect(result).toBeDefined(); + expect(result.user.id).toBe(user.id); + expect(result.token).toBeDefined(); + }); + + it('should reject invalid password', async () => { + const user = await createTestUser(); + + await expect( + userService.login({ email: user.email, password: 'wrongPassword' }) + ).rejects.toThrow('Invalid credentials'); + }); + + it('should reject non-existent email', async () => { + await expect( + userService.login({ email: 'nonexistent@example.com', password: 'password' }) + ).rejects.toThrow('Invalid credentials'); + }); + }); + + describe('getUserById', () => { + it('should return user without password', async () => { + const user = await createTestUser(); + + const result = await userService.getUserById(user.id); + + expect(result).toBeDefined(); + expect(result.id).toBe(user.id); + expect((result as any).password).toBeUndefined(); + }); + + it('should return null for non-existent user', async () => { + const result = await userService.getUserById('non-existent-id'); + expect(result).toBeNull(); + }); + }); + + describe('updateProfile', () => { + it('should update user profile', async () => { + const user = await createTestUser(); + const updateData = { + firstName: 'Updated', + lastName: 'Name', + phone: '+1234567890', + bio: 'Updated bio', + }; + + const updatedUser = await userService.updateProfile(user.id, updateData); + + expect(updatedUser).toMatchObject(updateData); + expect(updatedUser.id).toBe(user.id); + }); + + it('should throw error for non-existent user', async () => { + await expect( + userService.updateProfile('non-existent-id', { firstName: 'Test' }) + ).rejects.toThrow(); + }); + }); + + describe('getProfile', () => { + it('should get user profile without password', async () => { + const user = await createTestUser(); + + const profile = await userService.getProfile(user.id); + + expect(profile).toBeDefined(); + expect(profile.id).toBe(user.id); + expect(profile.email).toBe(user.email); + expect((profile as any).password).toBeUndefined(); + }); + + it('should throw error for non-existent user', async () => { + await expect( + userService.getProfile('non-existent-id') + ).rejects.toThrow(); + }); + }); + + describe('checkEmailExists', () => { + it('should return true for existing email', async () => { + const user = await createTestUser(); + + const exists = await userService.checkEmailExists(user.email); + expect(exists).toBe(true); + }); + + it('should return false for non-existent email', async () => { + const exists = await userService.checkEmailExists('nonexistent@example.com'); + expect(exists).toBe(false); + }); + + it('should be case insensitive', async () => { + const user = await createTestUser({ email: 'test@example.com' }); + + const exists = await userService.checkEmailExists('TEST@EXAMPLE.COM'); + expect(exists).toBe(true); + }); + }); + + describe('searchUsersByEmail', () => { + beforeEach(async () => { + await createTestUser({ email: 'john.doe@example.com' }); + await createTestUser({ email: 'jane.smith@example.com' }); + await createTestUser({ email: 'bob.johnson@company.com' }); + }); + + it('should search users by email pattern', async () => { + const results = await userService.searchUsersByEmail('example.com'); + + expect(results.length).toBe(2); + expect(results.every((user: any) => user.email.includes('example.com'))).toBe(true); + }); + + it('should return empty array for no matches', async () => { + const results = await userService.searchUsersByEmail('nonexistent.com'); + expect(results).toHaveLength(0); + }); + + it('should exclude passwords from search results', async () => { + const results = await userService.searchUsersByEmail('example.com'); + + results.forEach((user: any) => { + expect(user.password).toBeUndefined(); + }); + }); + }); + + describe('deleteProfile', () => { + it('should delete user profile', async () => { + const user = await createTestUser(); + + await userService.deleteProfile(user.id); + + const deletedUser = await prisma.user.findUnique({ + where: { id: user.id }, + }); + + expect(deletedUser).toBeNull(); + }); + + it('should throw error for non-existent user', async () => { + await expect( + userService.deleteProfile('non-existent-id') + ).rejects.toThrow(); + }); + }); + + describe('Error Handling and Edge Cases', () => { + it('should handle invalid email format', async () => { + await expect( + userService.register({ + email: 'invalid-email', + password: 'password123', + firstName: 'Test', + lastName: 'User', + }) + ).rejects.toThrow(); + }); + + it('should handle empty strings gracefully', async () => { + await expect( + userService.getUserById('') + ).rejects.toThrow(); + }); + + it('should handle null/undefined inputs', async () => { + await expect( + userService.getUserById(null as any) + ).rejects.toThrow(); + }); + + it('should validate required fields', async () => { + await expect( + userService.register({ + email: '', + password: '', + firstName: '', + lastName: '', + }) + ).rejects.toThrow(); + }); + }); +}); \ No newline at end of file diff --git a/tests/user.test.ts b/tests/user.test.ts new file mode 100755 index 0000000..ed7188e --- /dev/null +++ b/tests/user.test.ts @@ -0,0 +1,545 @@ +import bcrypt from 'bcryptjs'; +import express from 'express'; +import request from 'supertest'; +import { errorHandler } from '../src/middlewares/error.middleware'; +import userRoutes from '../src/routes/user.routes'; +import { createAuthHeaders, createTestUser, generateAuthToken } from './helpers'; +import { prisma } from './setup'; + +const app = express(); +app.use(express.json()); +app.use('/api/users', userRoutes); +app.use(errorHandler); + +describe('User Authentication & Management', () => { + describe('POST /api/users/register', () => { + it('should register a new user successfully', async () => { + const userData = { + email: 'test@example.com', + password: 'testPassword123', + firstName: 'John', + lastName: 'Doe', + }; + + const response = await request(app) + .post('/api/users/register') + .send(userData) + .expect(201); + + expect(response.body).toMatchObject({ + success: true, + data: { + user: { + email: userData.email, + firstName: userData.firstName, + lastName: userData.lastName, + }, + }, + }); + + expect(response.body.data.user.id).toBeDefined(); + expect(response.body.data.token).toBeDefined(); + expect(response.body.data.user.password).toBeUndefined(); + }); + + it('should return error for duplicate email', async () => { + const userData = { + email: 'duplicate@example.com', + password: 'testPassword123', + firstName: 'John', + lastName: 'Doe', + }; + + // Create user first + await createTestUser(userData); + + const response = await request(app) + .post('/api/users/register') + .send(userData) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('already exists'); + }); + + it('should validate required fields', async () => { + const invalidData = { + email: 'invalid-email', + password: '123', // Too short + }; + + const response = await request(app) + .post('/api/users/register') + .send(invalidData) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('validation'); + }); + }); + + describe('POST /api/users/login', () => { + it('should login with valid credentials', async () => { + const password = 'testPassword123'; + const user = await createTestUser({ + email: 'login@example.com', + password: await bcrypt.hash(password, 10), + }); + + const response = await request(app) + .post('/api/users/login') + .send({ + email: user.email, + password: password, + }) + .expect(200); + + expect(response.body).toMatchObject({ + success: true, + data: { + user: { + id: user.id, + email: user.email, + }, + }, + }); + + expect(response.body.data.token).toBeDefined(); + expect(response.body.data.user.password).toBeUndefined(); + }); + + it('should return error for invalid credentials', async () => { + const user = await createTestUser(); + + const response = await request(app) + .post('/api/users/login') + .send({ + email: user.email, + password: 'wrongPassword', + }) + .expect(401); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('Invalid credentials'); + }); + + it('should return error for non-existent user', async () => { + const response = await request(app) + .post('/api/users/login') + .send({ + email: 'nonexistent@example.com', + password: 'testPassword123', + }) + .expect(401); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('Invalid credentials'); + }); + }); + + describe('GET /api/users/profile', () => { + it('should get user profile with valid token', async () => { + const user = await createTestUser(); + const token = generateAuthToken(user.id); + + const response = await request(app) + .get('/api/users/profile') + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body).toMatchObject({ + success: true, + data: { + id: user.id, + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + }, + }); + + expect(response.body.data.password).toBeUndefined(); + }); + + it('should return error without token', async () => { + const response = await request(app) + .get('/api/users/profile') + .expect(401); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('No token provided'); + }); + + it('should return error with invalid token', async () => { + const response = await request(app) + .get('/api/users/profile') + .set(createAuthHeaders('invalid-token')) + .expect(401); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('Invalid token'); + }); + }); + + describe('PUT /api/users/profile', () => { + it('should update user profile successfully', async () => { + const user = await createTestUser(); + const token = generateAuthToken(user.id); + + const updateData = { + firstName: 'Updated', + lastName: 'Name', + phone: '+1234567890', + bio: 'Updated bio', + }; + + const response = await request(app) + .put('/api/users/profile') + .set(createAuthHeaders(token)) + .send(updateData) + .expect(200); + + expect(response.body).toMatchObject({ + success: true, + data: { + ...updateData, + id: user.id, + email: user.email, + }, + }); + + // Verify update in database + const updatedUser = await prisma.user.findUnique({ + where: { id: user.id }, + }); + + expect(updatedUser).toMatchObject(updateData); + }); + + it('should validate update data', async () => { + const user = await createTestUser(); + const token = generateAuthToken(user.id); + + const invalidData = { + email: 'invalid-email-format', + phone: 'invalid-phone', + }; + + const response = await request(app) + .put('/api/users/profile') + .set(createAuthHeaders(token)) + .send(invalidData) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('validation'); + }); + }); + + describe('Password Management', () => { + it('should change password successfully', async () => { + const user = await createTestUser(); + const token = generateAuthToken(user.id); + + const passwordData = { + currentPassword: 'testPassword123', + newPassword: 'newPassword456', + }; + + const response = await request(app) + .put('/api/users/password') + .set(createAuthHeaders(token)) + .send(passwordData) + .expect(200); + + expect(response.body).toMatchObject({ + success: true, + message: 'Password updated successfully', + }); + + // Verify password change by logging in with new password + const loginResponse = await request(app) + .post('/api/users/login') + .send({ + email: user.email, + password: 'newPassword456', + }) + .expect(200); + + expect(loginResponse.body.success).toBe(true); + }); + + it('should reject password change with wrong current password', async () => { + const user = await createTestUser(); + const token = generateAuthToken(user.id); + + const passwordData = { + currentPassword: 'wrongPassword', + newPassword: 'newPassword456', + }; + + const response = await request(app) + .put('/api/users/password') + .set(createAuthHeaders(token)) + .send(passwordData) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('current password'); + }); + + it('should validate new password requirements', async () => { + const user = await createTestUser(); + const token = generateAuthToken(user.id); + + const passwordData = { + currentPassword: 'testPassword123', + newPassword: '123', // Too short + }; + + const response = await request(app) + .put('/api/users/password') + .set(createAuthHeaders(token)) + .send(passwordData) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('validation'); + }); + }); + + describe('User Search and Listing', () => { + it('should search users by name or email', async () => { + const users = await Promise.all([ + createTestUser({ firstName: 'John', lastName: 'Doe', email: 'john@example.com' }), + createTestUser({ firstName: 'Jane', lastName: 'Smith', email: 'jane@example.com' }), + createTestUser({ firstName: 'Bob', lastName: 'Johnson', email: 'bob@example.com' }), + ]); + + const token = generateAuthToken(users[0].id); + + const response = await request(app) + .get('/api/users/search') + .query({ q: 'John' }) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data.length).toBe(2); // John Doe and Bob Johnson + }); + + it('should paginate search results', async () => { + // Create multiple test users + const users = []; + for (let i = 0; i < 15; i++) { + users.push(await createTestUser({ + firstName: `User${i}`, + email: `user${i}@example.com` + })); + } + + const token = generateAuthToken(users[0].id); + + const response = await request(app) + .get('/api/users/search') + .query({ q: 'User', page: 1, limit: 5 }) + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data.length).toBe(5); + expect(response.body.pagination).toBeDefined(); + expect(response.body.pagination.total).toBe(15); + }); + }); + + describe('Account Management', () => { + it('should deactivate user account', async () => { + const user = await createTestUser(); + const token = generateAuthToken(user.id); + + const response = await request(app) + .put('/api/users/deactivate') + .set(createAuthHeaders(token)) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.message).toContain('deactivated'); + + // Verify user cannot login after deactivation + const loginResponse = await request(app) + .post('/api/users/login') + .send({ + email: user.email, + password: 'testPassword123', + }) + .expect(401); + + expect(loginResponse.body.success).toBe(false); + }); + + it('should update last login timestamp', async () => { + const user = await createTestUser(); + + const loginResponse = await request(app) + .post('/api/users/login') + .send({ + email: user.email, + password: 'testPassword123', + }) + .expect(200); + + // Check that lastLoginAt was updated + const updatedUser = await prisma.user.findUnique({ + where: { id: user.id }, + }); + + expect(updatedUser?.lastLoginAt).toBeDefined(); + expect(new Date(updatedUser!.lastLoginAt!)).toBeInstanceOf(Date); + }); + }); + + describe('Data Validation', () => { + it('should validate email format during registration', async () => { + const invalidEmails = [ + 'invalid-email', + '@example.com', + 'test@', + 'test.example.com', + ]; + + for (const email of invalidEmails) { + const response = await request(app) + .post('/api/users/register') + .send({ + email, + password: 'testPassword123', + firstName: 'Test', + lastName: 'User', + }) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('validation'); + } + }); + + it('should validate required fields during registration', async () => { + const requiredFields = ['email', 'password', 'firstName', 'lastName']; + + for (const field of requiredFields) { + const userData = { + email: 'test@example.com', + password: 'testPassword123', + firstName: 'Test', + lastName: 'User', + }; + + delete userData[field as keyof typeof userData]; + + const response = await request(app) + .post('/api/users/register') + .send(userData) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('validation'); + } + }); + + it('should enforce password strength requirements', async () => { + const weakPasswords = [ + '123', // Too short + 'password', // No numbers + '12345678', // No letters + 'Pass1', // Too short + ]; + + for (const password of weakPasswords) { + const response = await request(app) + .post('/api/users/register') + .send({ + email: `test${Date.now()}@example.com`, + password, + firstName: 'Test', + lastName: 'User', + }) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('validation'); + } + }); + }); + + describe('Edge Cases and Error Handling', () => { + it('should handle duplicate email registration gracefully', async () => { + const userData = { + email: 'duplicate@example.com', + password: 'testPassword123', + firstName: 'First', + lastName: 'User', + }; + + // First registration should succeed + await request(app) + .post('/api/users/register') + .send(userData) + .expect(201); + + // Second registration with same email should fail + const response = await request(app) + .post('/api/users/register') + .send({ + ...userData, + firstName: 'Second', + }) + .expect(409); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('already exists'); + }); + + it('should handle malformed JSON in request body', async () => { + const response = await request(app) + .post('/api/users/register') + .set('Content-Type', 'application/json') + .send('{"invalid": json}') + .expect(400); + + expect(response.body.success).toBe(false); + }); + + it('should handle very long input data', async () => { + const longString = 'a'.repeat(1000); + + const response = await request(app) + .post('/api/users/register') + .send({ + email: 'test@example.com', + password: 'testPassword123', + firstName: longString, + lastName: 'User', + }) + .expect(400); + + expect(response.body.success).toBe(false); + expect(response.body.message).toContain('validation'); + }); + + it('should handle SQL injection attempts', async () => { + const maliciousEmail = "'; DROP TABLE users; --"; + + const response = await request(app) + .post('/api/users/register') + .send({ + email: maliciousEmail, + password: 'testPassword123', + firstName: 'Test', + lastName: 'User', + }) + .expect(400); + + expect(response.body.success).toBe(false); + }); + }); +}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..8b4d85d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,45 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "removeComments": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": false, + + // Style Options + // "noImplicitReturns": true, + // "noImplicitOverride": true, + // "noUnusedLocals": true, + // "noUnusedParameters": true, + // "noFallthroughCasesInSwitch": true, + // "noPropertyAccessFromIndexSignature": true, + + // Recommended Options + "strict": false, + "jsx": "react-jsx", + "verbatimModuleSyntax": false, + "isolatedModules": true, + "noUncheckedSideEffectImports": false, + "moduleDetection": "force", + "skipLibCheck": true, + }, + "ts-node": { + "esm": true + } +} diff --git a/validate-tests.js b/validate-tests.js new file mode 100755 index 0000000..34fa858 --- /dev/null +++ b/validate-tests.js @@ -0,0 +1,227 @@ +#!/usr/bin/env node + +/** + * Test Validation Script + * + * This script validates the entire test suite setup and provides + * a comprehensive health check for the testing infrastructure. + */ + +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +const COLORS = { + GREEN: '\x1b[32m', + RED: '\x1b[31m', + YELLOW: '\x1b[33m', + BLUE: '\x1b[34m', + RESET: '\x1b[0m', + BOLD: '\x1b[1m' +}; + +function log(message, color = COLORS.RESET) { + console.log(`${color}${message}${COLORS.RESET}`); +} + +function success(message) { + log(`✅ ${message}`, COLORS.GREEN); +} + +function error(message) { + log(`❌ ${message}`, COLORS.RED); +} + +function warning(message) { + log(`âš ī¸ ${message}`, COLORS.YELLOW); +} + +function info(message) { + log(`â„šī¸ ${message}`, COLORS.BLUE); +} + +function header(message) { + log(`\n${COLORS.BOLD}${COLORS.BLUE}=== ${message} ===${COLORS.RESET}\n`); +} + +function checkFileExists(filePath, description) { + if (fs.existsSync(filePath)) { + success(`${description} exists`); + return true; + } else { + error(`${description} missing: ${filePath}`); + return false; + } +} + +function runCommand(command, description, ignoreError = false) { + try { + info(`Running: ${description}`); + const output = execSync(command, { encoding: 'utf8', stdio: 'pipe' }); + success(`${description} completed successfully`); + return { success: true, output }; + } catch (err) { + if (ignoreError) { + warning(`${description} failed (ignored): ${err.message}`); + return { success: false, output: err.message }; + } else { + error(`${description} failed: ${err.message}`); + return { success: false, output: err.message }; + } + } +} + +async function validateTestSetup() { + let allValid = true; + + header('Validating Test Infrastructure'); + + // Check core test files + const testFiles = [ + ['tests/setup.ts', 'Test setup configuration'], + ['tests/helpers.ts', 'Test helper functions'], + ['tests/user.test.ts', 'User tests'], + ['tests/task.test.ts', 'Task tests'], + ['tests/team.test.ts', 'Team tests'], + ['tests/activity.test.ts', 'Activity tests'], + ['tests/user.service.test.ts', 'User service tests'], + ['.env.test', 'Test environment configuration'], + ['test-runner.js', 'Test runner script'], + ['jest.config.js', 'Jest configuration'] + ]; + + testFiles.forEach(([file, desc]) => { + if (!checkFileExists(file, desc)) { + allValid = false; + } + }); + + // Check package.json test scripts + header('Validating Package.json Scripts'); + + if (fs.existsSync('package.json')) { + const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')); + const requiredScripts = [ + 'test', + 'test:watch', + 'test:coverage', + 'test:ci', + 'test:full' + ]; + + requiredScripts.forEach(script => { + if (packageJson.scripts && packageJson.scripts[script]) { + success(`Script "${script}" is configured`); + } else { + error(`Script "${script}" is missing`); + allValid = false; + } + }); + } + + // Check dependencies + header('Validating Dependencies'); + + const result = runCommand('npm list jest @types/jest ts-jest supertest @types/supertest --depth=0', 'Checking test dependencies', true); + if (!result.success) { + warning('Some test dependencies may be missing. Please run: npm install'); + } + + // Check TypeScript compilation + header('Validating TypeScript Compilation'); + + const tscResult = runCommand('npx tsc --noEmit', 'TypeScript compilation check'); + if (!tscResult.success) { + allValid = false; + } + + // Check database connectivity (if possible) + header('Validating Database Setup'); + + if (fs.existsSync('.env.test')) { + success('Test environment file exists'); + const envContent = fs.readFileSync('.env.test', 'utf8'); + if (envContent.includes('DATABASE_URL')) { + success('Database URL configured in test environment'); + } else { + error('DATABASE_URL missing in .env.test'); + allValid = false; + } + } + + // Check Prisma setup + const prismaResult = runCommand('npx prisma generate', 'Prisma client generation', true); + if (prismaResult.success) { + success('Prisma client generated successfully'); + } + + // Validate Jest configuration + header('Validating Jest Configuration'); + + if (fs.existsSync('jest.config.js')) { + try { + const jestConfig = require('./jest.config.js'); + + if (jestConfig.testEnvironment === 'node') { + success('Jest environment configured for Node.js'); + } + + if (jestConfig.setupFilesAfterEnv && jestConfig.setupFilesAfterEnv.includes('./tests/setup.ts')) { + success('Jest setup file configured'); + } + + if (jestConfig.collectCoverageFrom) { + success('Coverage collection configured'); + } + + } catch (err) { + error(`Jest configuration error: ${err.message}`); + allValid = false; + } + } + + // Run a quick test validation + header('Running Quick Test Validation'); + + const quickTestResult = runCommand('npm test -- --passWithNoTests --testTimeout=10000', 'Quick test run', true); + if (quickTestResult.success) { + success('Test framework is working correctly'); + } else { + warning('Test framework may have issues. Run full tests for detailed diagnostics.'); + } + + // Final summary + header('Validation Summary'); + + if (allValid) { + success('🎉 All validations passed! Your test setup is ready.'); + info('\nNext steps:'); + info('1. Run "npm test" to execute all tests'); + info('2. Run "npm run test:coverage" to see coverage report'); + info('3. Run "npm run test:watch" for development mode'); + info('4. Check TESTING.md for comprehensive documentation'); + } else { + error('❌ Some validations failed. Please address the issues above.'); + info('\nCommon fixes:'); + info('1. Run "npm install" to install missing dependencies'); + info('2. Ensure PostgreSQL is running and test database exists'); + info('3. Check .env.test configuration'); + info('4. Run "npx prisma generate" to generate Prisma client'); + } + + return allValid; +} + +// Run validation +if (require.main === module) { + validateTestSetup() + .then(success => { + process.exit(success ? 0 : 1); + }) + .catch(err => { + error(`Validation failed: ${err.message}`); + process.exit(1); + }); +} + +module.exports = { validateTestSetup }; \ No newline at end of file