commit 18e0e047005d4ff5c3ba7e8860533c568d9a2b64 Author: Lorenzo Sanesi Date: Thu Mar 26 22:46:03 2026 +0100 Initial commit diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..428d87b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,186 @@ +# Changelog - Scommetti App + +Questo file tiene traccia di tutte le modifiche ad alto livello del progetto. + +## Struttura del Progetto + +``` +scomettiapp/ +├── backend-node/ # Backend Node.js con Express +├── frontend-angular/ # Frontend Angular 20 +├── frontend-flutter/ # Frontend Flutter (placeholder) +└── docker-compose.yml # Orchestrazione dei servizi +``` + +--- + +## ✅ Completato + +### Infrastruttura Base +- [x] Setup struttura progetto con backend Node.js e frontend Angular +- [x] Configurazione Docker Compose per orchestrazione servizi +- [x] Setup database PostgreSQL per dati relazionali (utenti, gruppi, amicizie) +- [x] Setup database MongoDB per scommesse +- [x] Script di seed per popolamento dati di test + +### Backend (Node.js + Express) + +#### Database & Modelli +- [x] Modello User (PostgreSQL) - id, name, email, avatarUrl, score +- [x] Modello Group (PostgreSQL) - id, name +- [x] Modello Friendship (PostgreSQL) - gestione amicizie bidirezionali +- [x] Modello FriendRequest (PostgreSQL) - gestione richieste di amicizia +- [x] Modello Bet (MongoDB) - scommesse con partecipanti, stato, posta in gioco +- [x] Associazioni many-to-many tra User e Group +- [x] Schema SQL di inizializzazione database (init.sql) + +#### API Endpoints +- [x] `/api/initial-data` - Caricamento dati iniziali (utente corrente, amici, gruppi) +- [x] `/api/users` - Gestione utenti +- [x] `/api/groups` - CRUD gruppi +- [x] `/api/groups/:id/members` - Gestione membri dei gruppi +- [x] `/api/bets` - CRUD scommesse +- [x] `/api/bets/by-time` - Scommesse filtrate per periodo temporale +- [x] `/api/bets/by-group/:groupId` - Scommesse per gruppo +- [x] `/api/friends` - Lista amici +- [x] `/api/friends/requests` - Richieste di amicizia (GET, POST, PATCH) + +#### Configurazione +- [x] Server Express con middleware CORS +- [x] Connessione doppio database (PostgreSQL + MongoDB) +- [x] Seed automatico al primo avvio +- [x] Gestione variabili ambiente (PORT, MONGO_URI, POSTGRES_URI) + +### Frontend (Angular 20) + +#### Servizi +- [x] `ApiService` - Comunicazione HTTP con backend +- [x] `DataService` - Gestione stato applicazione con RxJS signals + +#### Componenti Principali +- [x] `AppComponent` - Layout principale con navigation bar +- [x] `HomeComponent` - Dashboard principale con visualizzazione scommesse + - [x] `BetsByTimeComponent` - Filtro scommesse per periodo (oggi, settimana, mese) + - [x] `BetsByGroupComponent` - Filtro scommesse per gruppo +- [x] `DiscoverComponent` - Scoperta scommesse pubbliche +- [x] `CreateBetComponent` - Creazione nuove scommesse +- [x] `GroupsComponent` - Gestione gruppi + - [x] `GroupDetailComponent` - Dettaglio singolo gruppo +- [x] `ProfileComponent` - Profilo utente + - [x] `EditProfileComponent` - Modifica dati profilo +- [x] `BetCardComponent` - Card riutilizzabile per visualizzare scommesse + +#### UI/UX +- [x] Design responsive con Tailwind CSS +- [x] Bottom navigation con 5 sezioni (Home, Scopri, Crea, Gruppi, Profilo) +- [x] Sistema di routing client-side tra viste +- [x] Tema dark con palette slate/cyan + +#### Modelli TypeScript +- [x] Modelli TypeScript per tutte le entità (Bet, User, Group, ecc.) +- [x] Enum per stati scommesse (ACTIVE, AWAITING_VALIDATION, COMPLETED) +- [x] Enum per ruoli partecipanti (PARTICIPANT, OBSERVER) + +--- + +## 📋 Da Fare + +### Backend + +#### Autenticazione & Sicurezza +- [ ] Sistema di autenticazione (JWT/OAuth) +- [ ] Middleware di protezione delle route +- [ ] Validazione input utente +- [ ] Rate limiting +- [ ] Gestione sessioni utente + +#### Funzionalità Scommesse +- [ ] Logica di validazione vincitori +- [ ] Sistema di votazione per vincitori (per scommesse tra amici) +- [ ] Notifiche per eventi scommessa (inizio, fine, vincitore) +- [ ] Cronologia scommesse completate +- [ ] Statistiche utente (win rate, streak, ecc.) + +#### Funzionalità Sociali +- [ ] Chat/commenti nelle scommesse +- [ ] Notifiche in tempo reale +- [ ] Sistema di reputazione/badge +- [ ] Feed attività amici + +#### Ottimizzazioni +- [ ] Caching con Redis +- [ ] Paginazione risultati +- [ ] Query optimization +- [ ] File upload per avatar personalizzati +- [ ] Compressione risposte HTTP + +### Frontend + +#### Funzionalità Mancanti +- [ ] Visualizzazione dettaglio scommessa +- [ ] Modifica/eliminazione scommessa +- [ ] Gestione richieste di amicizia (UI) +- [ ] Ricerca utenti +- [ ] Notifiche push +- [ ] Stati di caricamento e gestione errori +- [ ] Conferme azioni critiche (elimina, ecc.) + +#### UX Improvements +- [ ] Animazioni transizioni tra viste +- [ ] Skeleton loaders +- [ ] Pull to refresh +- [ ] Infinite scroll per liste lunghe +- [ ] Filtri avanzati scommesse +- [ ] Ordinamento personalizzato + +#### Testing +- [ ] Unit test per componenti +- [ ] Integration test per servizi +- [ ] E2E test per flussi principali + +### DevOps & Deployment +- [ ] CI/CD pipeline +- [ ] Health check endpoints +- [ ] Logging strutturato +- [ ] Monitoring e alerting +- [ ] Backup automatici database +- [ ] Documentazione API (Swagger/OpenAPI) +- [ ] Docker production-ready configuration +- [ ] Environment separati (dev, staging, prod) + +### Flutter App +- [ ] Inizializzare progetto Flutter +- [ ] Implementare le stesse funzionalità del frontend Angular +- [ ] Setup build per iOS/Android + +--- + +## 🔮 Possibili Sviluppi Futuri +- [ ] Integrazione con provider di pagamento per scommesse con denaro reale +- [ ] Gamification avanzata (livelli, achievement) +- [ ] Scommesse su eventi pubblici (sport, politica, ecc.) +- [ ] Marketplace di scommesse +- [ ] Analytics dashboard per amministratori +- [ ] API pubblica per integrazioni esterne +- [ ] App mobile nativa (Flutter) +- [ ] Versione web pubblica (landing page) + +--- + +## Note Tecniche + +### Stack Tecnologico +- **Backend**: Node.js, Express, Sequelize (PostgreSQL), Mongoose (MongoDB) +- **Frontend**: Angular 20, RxJS, Tailwind CSS, Vite +- **Database**: PostgreSQL (dati relazionali), MongoDB (scommesse) +- **DevOps**: Docker, Docker Compose + +### Porte +- Backend API: `3000` +- Frontend Dev: `4200` +- PostgreSQL: `5432` +- MongoDB: `27017` + +### Database +- PostgreSQL contiene: Users, Groups, Friendships, FriendRequests +- MongoDB contiene: Bets (per maggiore flessibilità dello schema) diff --git a/backend-node/.dockerignore b/backend-node/.dockerignore new file mode 100644 index 0000000..9e35a76 --- /dev/null +++ b/backend-node/.dockerignore @@ -0,0 +1,56 @@ +# Node +node_modules +npm-debug.log +yarn-error.log +package-lock.json +yarn.lock + +# Environment +.env +.env.local +.env.*.local + +# Logs +logs +*.log + +# Testing +coverage +.nyc_output + +# IDE +.vscode +.idea +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Git +.git +.gitignore +.gitattributes + +# Documentation +*.md +!README.md +docs/ + +# CI/CD +.github +.gitlab-ci.yml +.travis.yml + +# Docker +Dockerfile +docker-compose*.yml +.dockerignore + +# Misc +*.bak +*.tmp +*.temp +.cache diff --git a/backend-node/.env.example b/backend-node/.env.example new file mode 100644 index 0000000..48ae42a --- /dev/null +++ b/backend-node/.env.example @@ -0,0 +1,108 @@ +# ======================================== +# ENVIRONMENT CONFIGURATION TEMPLATE +# ======================================== +# Copy this file to .env and fill in your values +# DO NOT commit .env to version control + +# ======================================== +# Application Configuration +# ======================================== +NODE_ENV=development +PORT=3000 + +# ======================================== +# PostgreSQL Database +# ======================================== +POSTGRES_HOST=localhost +POSTGRES_PORT=5432 +POSTGRES_DB=amici_scommesse +POSTGRES_USER=admin +POSTGRES_PASSWORD=admin +POSTGRES_URI=postgres://admin:admin@localhost:5432/amici_scommesse + +# ======================================== +# MongoDB Database +# ======================================== +MONGO_HOST=localhost +MONGO_PORT=27017 +MONGO_DB=amici-scommesse +MONGO_USER=root +MONGO_PASSWORD=root@servovio +MONGO_AUTH_SOURCE=admin +MONGO_URI=mongodb://root:root@servovio@localhost:27017/amici-scommesse?authSource=admin + +# ======================================== +# Logging +# ======================================== +LOG_LEVEL=debug +ENABLE_SQL_LOGGING=false + +# ======================================== +# CORS Configuration +# ======================================== +CORS_ORIGIN=http://localhost:5173 +ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3001 + +# ======================================== +# Mock Authentication (Development Only) +# ======================================== +MOCK_USER_NAME=Mario Rossi + +# ======================================== +# API Configuration +# ======================================== +API_VERSION=v1 +API_PREFIX=/api + +# ======================================== +# Database Sync (Development Only) +# ======================================== +# WARNING: force: true drops all tables and recreates them +# Set to false in production and use migrations instead +DB_SYNC_FORCE=true + +# ======================================== +# Future: JWT Authentication (Not Yet Implemented) +# ======================================== +# JWT_SECRET=your-secret-key-change-this +# JWT_EXPIRES_IN=7d +# JWT_REFRESH_EXPIRES_IN=30d + +# ======================================== +# Future: Rate Limiting (Not Yet Implemented) +# ======================================== +# RATE_LIMIT_WINDOW_MS=900000 +# RATE_LIMIT_MAX_REQUESTS=100 + +# ======================================== +# Future: Redis (Not Yet Implemented) +# ======================================== +# REDIS_HOST=localhost +# REDIS_PORT=6379 +# REDIS_PASSWORD= +# REDIS_DB=0 + +# ======================================== +# Future: File Upload (Not Yet Implemented) +# ======================================== +# UPLOAD_MAX_FILE_SIZE=5242880 +# UPLOAD_ALLOWED_TYPES=image/jpeg,image/png,image/gif +# AWS_S3_BUCKET= +# AWS_S3_REGION= +# AWS_ACCESS_KEY_ID= +# AWS_SECRET_ACCESS_KEY= + +# ======================================== +# Future: Email Service (Not Yet Implemented) +# ======================================== +# SMTP_HOST=smtp.gmail.com +# SMTP_PORT=587 +# SMTP_USER= +# SMTP_PASSWORD= +# EMAIL_FROM=noreply@amiciscommesse.com + +# ======================================== +# Future: Monitoring & Analytics (Not Yet Implemented) +# ======================================== +# SENTRY_DSN= +# ANALYTICS_ENABLED=false diff --git a/backend-node/.eslintignore b/backend-node/.eslintignore new file mode 100644 index 0000000..06c3eac --- /dev/null +++ b/backend-node/.eslintignore @@ -0,0 +1,3 @@ +node_modules/ +coverage/ +dist/ diff --git a/backend-node/.eslintrc.json b/backend-node/.eslintrc.json new file mode 100644 index 0000000..b768832 --- /dev/null +++ b/backend-node/.eslintrc.json @@ -0,0 +1,21 @@ +{ + "env": { + "node": true, + "es2021": true + }, + "extends": ["airbnb-base", "prettier"], + "plugins": ["prettier"], + "parserOptions": { + "ecmaVersion": "latest" + }, + "rules": { + "prettier/prettier": "error", + "no-console": "off", + "consistent-return": "warn", + "no-underscore-dangle": ["error", { "allow": ["_id"] }], + "import/no-extraneous-dependencies": [ + "error", + { "devDependencies": true } + ] + } +} diff --git a/backend-node/.prettierrc b/backend-node/.prettierrc new file mode 100644 index 0000000..dc6aab3 --- /dev/null +++ b/backend-node/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2, + "arrowParens": "avoid" +} diff --git a/backend-node/API_DOCUMENTATION.md b/backend-node/API_DOCUMENTATION.md new file mode 100644 index 0000000..a74080f --- /dev/null +++ b/backend-node/API_DOCUMENTATION.md @@ -0,0 +1,431 @@ +# API Documentation - Amici Scommesse + +## 📚 Overview + +Documentazione completa dell'API Amici Scommesse v1.1.0. + +**Base URL**: `http://localhost:3000/api/v1` + +## 🔗 Specifica OpenAPI + +La specifica OpenAPI completa è disponibile nel file `openapi.yaml`. + +### Visualizzare la Specifica + +**Opzione 1: Swagger UI Online** +1. Vai su https://editor.swagger.io/ +2. File → Import File → Seleziona `openapi.yaml` +3. Visualizza la documentazione interattiva + +**Opzione 2: Swagger UI Locale** +```bash +npx swagger-ui-watcher openapi.yaml +``` + +**Opzione 3: Redoc** +```bash +npx @redocly/cli preview-docs openapi.yaml +``` + +### Generare Client SDK + +**TypeScript/JavaScript:** +```bash +npx @openapitools/openapi-generator-cli generate \ + -i openapi.yaml \ + -g typescript-axios \ + -o ./frontend/src/api +``` + +**Python:** +```bash +openapi-generator-cli generate \ + -i openapi.yaml \ + -g python \ + -o ./python-client +``` + +## 🌐 Endpoints API v1 + +### 🎯 Groups + +| Metodo | Endpoint | Descrizione | +|--------|----------|-------------| +| GET | `/groups` | Lista tutti i gruppi | +| GET | `/groups/:id` | Dettaglio gruppo | +| POST | `/groups` | Crea gruppo | +| PUT | `/groups/:id` | Aggiorna gruppo | +| DELETE | `/groups/:id` | Elimina gruppo | +| PUT | `/groups/:id/members` | Aggiungi membri | +| DELETE | `/groups/:id/members/:userId` | Rimuovi membro | + +### 🎲 Bets + +| Metodo | Endpoint | Descrizione | +|--------|----------|-------------| +| GET | `/bets` | Lista tutte le scommesse | +| GET | `/bets/:id` | Dettaglio scommessa | +| POST | `/bets` | Crea scommessa | +| PUT | `/bets/:id` | Aggiorna scommessa | +| DELETE | `/bets/:id` | Elimina scommessa | + +### 👥 Friend Requests + +| Metodo | Endpoint | Descrizione | +|--------|----------|-------------| +| GET | `/friend-requests` | Lista amici | +| GET | `/friend-requests/pending` | Richieste pending | +| POST | `/friend-requests` | Invia richiesta | +| POST | `/friend-requests/:id/accept` | Accetta richiesta | +| DELETE | `/friend-requests/:id` | Rifiuta richiesta | + +### 👤 Users + +| Metodo | Endpoint | Descrizione | +|--------|----------|-------------| +| PUT | `/users/current` | Aggiorna profilo | + +### 📦 Initial Data + +| Metodo | Endpoint | Descrizione | +|--------|----------|-------------| +| GET | `/initial-data` | Carica dati iniziali | + +### ❤️ Health + +| Metodo | Endpoint | Descrizione | +|--------|----------|-------------| +| GET | `/health` | Health check database | + +## 📝 Formato Risposte + +### Successo (2xx) + +```json +{ + "success": true, + "message": "Operation successful", + "data": { ... }, + "timestamp": "2024-01-01T00:00:00.000Z" +} +``` + +### Errore (4xx, 5xx) + +```json +{ + "success": false, + "message": "Error message", + "errors": [ + { + "field": "name", + "message": "Name is required" + } + ], + "timestamp": "2024-01-01T00:00:00.000Z" +} +``` + +## 🔐 Autenticazione + +**Attuale**: Mock authentication (Mario Rossi hardcoded) +**Prossimo**: JWT authentication da implementare + +## 🧪 Esempi di Chiamate + +### Creare un Gruppo + +```bash +curl -X POST http://localhost:3000/api/v1/groups \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Gruppo Calcio", + "members": [1, 2, 3] + }' +``` + +Risposta: +```json +{ + "success": true, + "message": "Group created successfully", + "data": { + "id": 1, + "name": "Gruppo Calcio", + "members": [ + { + "id": 1, + "name": "Mario Rossi", + "email": "mario.rossi@example.com", + "avatarUrl": null, + "score": 0 + } + ] + }, + "timestamp": "2024-01-01T12:00:00.000Z" +} +``` + +### Creare una Scommessa + +```bash +curl -X POST http://localhost:3000/api/v1/bets \ + -H "Content-Type: application/json" \ + -d '{ + "title": "Chi vince il campionato?", + "description": "Scommessa sul vincitore del campionato 2024", + "stake": "50 euro", + "participants": [ + { + "user": 1, + "role": "PARTICIPANT" + }, + { + "user": 2, + "role": "PARTICIPANT" + } + ], + "isPublic": false, + "groupId": "1" + }' +``` + +### Inviare Richiesta di Amicizia + +```bash +curl -X POST http://localhost:3000/api/v1/friend-requests \ + -H "Content-Type: application/json" \ + -d '{ + "identifier": 2 + }' +``` + +oppure con email: + +```bash +curl -X POST http://localhost:3000/api/v1/friend-requests \ + -H "Content-Type: application/json" \ + -d '{ + "identifier": "luigi.verdi@example.com" + }' +``` + +### Aggiornare Profilo Utente + +```bash +curl -X PUT http://localhost:3000/api/v1/users/current \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Mario Rossi", + "email": "mario.rossi.new@example.com", + "avatarUrl": "https://example.com/avatar.jpg" + }' +``` + +### Caricare Dati Iniziali + +```bash +curl http://localhost:3000/api/v1/initial-data +``` + +### Health Check + +```bash +curl http://localhost:3000/health +``` + +Risposta: +```json +{ + "status": "healthy", + "timestamp": "2024-01-01T12:00:00.000Z", + "services": { + "postgres": "connected", + "mongodb": "connected" + } +} +``` + +## 🔄 Migrazione da API Legacy + +### API Legacy (Backward Compatibility) + +Le vecchie route sono ancora disponibili su `/api/legacy/*` per compatibilità: + +``` +/api/legacy/groups → legacy format +/api/legacy/bets → legacy format +/api/legacy/friend-requests → legacy format +``` + +### Differenze tra v1 e Legacy + +| Aspetto | Legacy | v1 | +|---------|--------|-----| +| Formato risposta | Varia | Standardizzato | +| Validazione | Inline | Joi schema | +| Error handling | Inconsistente | Centralizzato | +| Success status | Varia | Sempre con wrapper | +| Timestamp | No | Sì | + +### Piano di Migrazione Frontend + +1. **Fase 1**: Testare endpoint v1 in sviluppo +2. **Fase 2**: Migrare chiamate API da `/api/*` a `/api/v1/*` +3. **Fase 3**: Aggiornare gestione risposte (nuovo formato) +4. **Fase 4**: Gestire nuovi errori standardizzati +5. **Fase 5**: Deprecare chiamate legacy + +## 📊 Codici di Stato HTTP + +| Codice | Significato | Quando | +|--------|-------------|--------| +| 200 | OK | Operazione riuscita | +| 201 | Created | Risorsa creata | +| 400 | Bad Request | Validation error | +| 404 | Not Found | Risorsa non trovata | +| 409 | Conflict | Conflitto (es: già amici) | +| 500 | Internal Error | Errore server | +| 503 | Service Unavailable | Database non disponibile | + +## 🛠️ Testing con Postman + +### Importare Specifica OpenAPI + +1. Apri Postman +2. Import → Link → Incolla URL o File +3. Seleziona `openapi.yaml` +4. Postman genera automaticamente la collection + +### Environment Variables + +Crea un environment Postman con: +```json +{ + "baseUrl": "http://localhost:3000/api/v1", + "legacyUrl": "http://localhost:3000/api/legacy" +} +``` + +## 🚀 Integrare con Frontend + +### Axios Example (TypeScript) + +```typescript +import axios from 'axios'; + +const api = axios.create({ + baseURL: 'http://localhost:3000/api/v1', + headers: { + 'Content-Type': 'application/json', + }, +}); + +// Response Interceptor per formato standardizzato +api.interceptors.response.use( + (response) => response.data, // Ritorna solo { success, message, data, timestamp } + (error) => { + if (error.response) { + // Server ha risposto con errore + return Promise.reject(error.response.data); + } + return Promise.reject(error); + } +); + +// Esempio uso +async function createGroup(name: string, members: number[]) { + try { + const response = await api.post('/groups', { name, members }); + console.log('Success:', response.data); + return response.data; + } catch (error) { + console.error('Error:', error.message, error.errors); + throw error; + } +} +``` + +### Fetch Example (JavaScript) + +```javascript +async function createGroup(name, members) { + const response = await fetch('http://localhost:3000/api/v1/groups', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ name, members }), + }); + + const result = await response.json(); + + if (!result.success) { + throw new Error(result.message); + } + + return result.data; +} +``` + +### React Hook Example + +```typescript +import { useState, useEffect } from 'react'; +import api from './api'; + +function useGroups() { + const [groups, setGroups] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + async function fetchGroups() { + setLoading(true); + try { + const response = await api.get('/groups'); + setGroups(response.data); + } catch (err) { + setError(err); + } finally { + setLoading(false); + } + } + fetchGroups(); + }, []); + + return { groups, loading, error }; +} +``` + +## 🐛 Troubleshooting + +### Errore 404 su endpoint v1 + +Verifica che il server sia avviato e che stai usando `/api/v1/` e non `/api/`. + +### Validation errors + +Controlla la specifica OpenAPI per i campi required e i formati accettati. + +### CORS errors + +Il server ha CORS abilitato. Se riscontri problemi, verifica la configurazione del frontend. + +### Health check fallisce + +Verifica che PostgreSQL e MongoDB siano in esecuzione e accessibili. + +## 📞 Supporto + +Per problemi o domande: +- Consulta la specifica OpenAPI completa in `openapi.yaml` +- Vedi esempi in `IMPLEMENTATION_GUIDE.md` +- Controlla il CHANGELOG.md per ultime modifiche + +## 🔗 Link Utili + +- [Swagger Editor](https://editor.swagger.io/) +- [OpenAPI Generator](https://openapi-generator.tech/) +- [Postman](https://www.postman.com/) +- [Redoc](https://redocly.com/redoc/) diff --git a/backend-node/CHANGELOG.md b/backend-node/CHANGELOG.md new file mode 100644 index 0000000..42297b0 --- /dev/null +++ b/backend-node/CHANGELOG.md @@ -0,0 +1,417 @@ +# Changelog - Backend Node.js + +Tracciamento modifiche specifiche del backend. + +--- + +## ✅ Completato + +### Setup Iniziale +- [x] Inizializzazione progetto Node.js con npm +- [x] Installazione dipendenze: Express, Sequelize, Mongoose, CORS, pg +- [x] Configurazione server Express (server.js) +- [x] Setup doppia connessione database (PostgreSQL + MongoDB) + +### Database PostgreSQL + +#### Modelli Sequelize +- [x] **User Model** (`models/user.model.js`) + - Campi: id, name, email, avatarUrl, score + - Validazioni email unique + - Relazioni: belongsToMany Groups, hasMany FriendRequests + +- [x] **Group Model** (`models/group.model.js`) + - Campi: id, name + - Relazioni: belongsToMany Users (many-to-many) + +- [x] **Friendship Model** (`models/friendship.model.js`) + - Campi: id, user1Id, user2Id + - Vincolo: user1Id < user2Id (evita duplicati) + - Hook beforeValidate per garantire ordine corretto + +- [x] **FriendRequest Model** (`models/friendRequest.model.js`) + - Campi: id, fromUserId, toUserId, status, createdAt, updatedAt + - Stati: PENDING, ACCEPTED, REJECTED + - Timestamps automatici + +- [x] **Associations** (`models/associations.js`) + - User ↔ Group (many-to-many via UserGroups) + - User → FriendRequest (fromUser, toUser) + - Friendship → User (user1, user2) + +#### Schema SQL +- [x] Script di inizializzazione (`init.sql`) +- [x] Definizione tabelle con constraint e foreign keys +- [x] Indici per performance su ricerche frequenti +- [x] Check constraints per data integrity + +### Database MongoDB + +- [x] **Bet Model** (`models/bet.model.js`) + - Schema: title, description, stake, participants[], status, startDate, endDate, isPublic, groupId, winner + - Partecipanti con ruolo (PARTICIPANT/OBSERVER) + - Stati: ACTIVE, AWAITING_VALIDATION, COMPLETED + - Timestamp automatici (createdAt, updatedAt) + +### API Routes + +#### Initial Data Route (`routes/initial-data.routes.js`) +- [x] `GET /api/initial-data` - Carica dati iniziali per l'app + - Utente corrente (hardcoded userId=1 per ora) + - Lista amici con info complete + - Gruppi dell'utente con membri + - Scommesse dell'utente (tutte, attive, passate) + +#### Users Routes (`routes/users.routes.js`) +- [x] `GET /api/users` - Lista tutti gli utenti +- [x] `GET /api/users/:id` - Dettaglio utente singolo + +#### Groups Routes (`routes/groups.routes.js`) +- [x] `GET /api/groups` - Lista tutti i gruppi +- [x] `GET /api/groups/:id` - Dettaglio gruppo con membri +- [x] `POST /api/groups` - Crea nuovo gruppo +- [x] `PUT /api/groups/:id` - Aggiorna gruppo +- [x] `DELETE /api/groups/:id` - Elimina gruppo +- [x] `POST /api/groups/:id/members` - Aggiungi membro al gruppo +- [x] `DELETE /api/groups/:id/members/:userId` - Rimuovi membro + +#### Bets Routes (`routes/bets.routes.js`) +- [x] `GET /api/bets` - Lista tutte le scommesse +- [x] `GET /api/bets/:id` - Dettaglio scommessa +- [x] `POST /api/bets` - Crea nuova scommessa +- [x] `PUT /api/bets/:id` - Aggiorna scommessa +- [x] `DELETE /api/bets/:id` - Elimina scommessa +- [x] `GET /api/bets/by-time` - Filtra scommesse per periodo (today, week, month) +- [x] `GET /api/bets/by-group/:groupId` - Scommesse per gruppo specifico + +#### Friends Routes (`routes/friends.routes.js`) +- [x] `GET /api/friends/:userId` - Lista amici di un utente +- [x] `GET /api/friends/requests/:userId` - Richieste di amicizia pending +- [x] `POST /api/friends/requests` - Invia richiesta di amicizia +- [x] `PATCH /api/friends/requests/:requestId` - Accetta/rifiuta richiesta + +#### API Router (`routes/api.js`) +- [x] Router factory che accetta istanza Sequelize +- [x] Composizione di tutti i sub-router + +### Utilities +- [x] **Seed Script** (`seed.js`) + - Popola PostgreSQL con 6 utenti di test + - Crea 3 amicizie e 2 richieste pending + - Crea 2 gruppi con membri + - Popola MongoDB con 5 scommesse di esempio + - Skip automatico se database già popolato + +- [x] **Populate Bets** (`utils/populateBets.js`) + - Utility per popolare scommesse (se necessario) + +### Configurazione +- [x] Sync automatico con `{ force: true }` in development +- [x] Seed automatico al primo avvio +- [x] CORS abilitato per frontend +- [x] JSON body parser +- [x] Gestione errori connessione database + +--- + +## 📋 Da Fare + +### Autenticazione & Autorizzazione +- [ ] Implementare JWT authentication +- [ ] Middleware di protezione route +- [ ] Login/Register endpoints +- [ ] Password hashing (bcrypt) +- [ ] Refresh token mechanism +- [ ] OAuth2 integration (Google, Facebook) +- [ ] Gestione sessioni + +### Validazione & Sicurezza +- [ ] Validation middleware con Joi/express-validator +- [ ] Sanitizzazione input utente +- [ ] Rate limiting per API +- [ ] CORS configuration più restrittiva per production +- [ ] Helmet.js per security headers +- [ ] SQL injection prevention (già parzialmente coperto da Sequelize) +- [ ] XSS protection + +### Funzionalità Scommesse +- [ ] Endpoint per dichiarare vincitore +- [ ] Sistema di votazione vincitori (per scommesse tra amici) +- [ ] Logica di calcolo punteggi utenti +- [ ] Notifiche per eventi scommessa +- [ ] Endpoint statistiche utente + - Win rate + - Streak attuali + - Scommesse vinte/perse + - Classifica generale +- [ ] Filtri avanzati per ricerca scommesse +- [ ] Paginazione scommesse +- [ ] Cronologia completa scommesse + +### Funzionalità Sociali +- [ ] Endpoint per chat/commenti nelle scommesse +- [ ] WebSocket/SSE per notifiche real-time +- [ ] Sistema di notifiche persistenti +- [ ] Feed attività (chi ha creato cosa, chi ha vinto, ecc.) +- [ ] Richieste di amicizia: suggerimenti basati su amici in comune +- [ ] Blocco utenti +- [ ] Report/segnalazioni + +### Funzionalità Gruppi +- [ ] Ruoli nei gruppi (admin, membro) +- [ ] Inviti al gruppo +- [ ] Statistiche gruppo +- [ ] Impostazioni privacy gruppo + +### File & Media +- [ ] Upload avatar utente +- [ ] Upload immagini nelle scommesse +- [ ] Integrazione storage (S3, Cloudinary) +- [ ] Resize/compression immagini + +### Performance & Optimization +- [ ] Caching con Redis + - Cache lista amici + - Cache gruppi utente + - Cache scommesse popolari +- [ ] Query optimization +- [ ] Indici database aggiuntivi dove necessario +- [ ] Paginazione su tutti gli endpoint liste +- [ ] Response compression (gzip) +- [ ] Database connection pooling optimization + +### Testing +- [ ] Unit test per modelli +- [ ] Integration test per API endpoints +- [ ] Test copertura almeno 80% +- [ ] Mock database per test +- [ ] Setup test runner (Jest/Mocha) + +### DevOps +- [ ] Logging strutturato (Winston/Pino) +- [ ] Health check endpoint (`/health`) +- [ ] Metrics endpoint (`/metrics`) +- [ ] Error tracking (Sentry) +- [ ] Environment variables validation +- [ ] Graceful shutdown +- [ ] Dockerfile per production +- [ ] Docker multi-stage build + +### Database +- [ ] Migration system (invece di sync force) +- [ ] Backup automatici +- [ ] Soft delete per entità critiche +- [ ] Audit log per modifiche importanti +- [ ] Database indexing strategy review + +### API Documentation +- [ ] Swagger/OpenAPI specification +- [ ] Endpoint documentation con esempi +- [ ] Postman collection +- [ ] API versioning strategy + +### Monitoring +- [ ] Application Performance Monitoring (APM) +- [ ] Database query monitoring +- [ ] Error rate alerts +- [ ] Response time monitoring + +--- + +## 🐛 Bug Conosciuti +- Nessuno al momento + +--- + +## 🔧 Miglioramenti Tecnici Completati (v1.1.0 - 2024) + +### Refactoring Architetturale +- [x] **Separazione logica business dai controller (service layer)** + - Creati 5 services: users, groups, friends, bets, data-stitching + - Business logic completamente spostata dai controller ai services + - `services/` directory con pattern consistente + +- [x] **Dependency Injection Container** + - Implementato Awilix per DI + - Container setup in `di/container.js` + - Registrazione automatica di repositories, services, controllers + - Lifetime management: singleton per database, scoped per services + +- [x] **Request/Response DTOs** + - Validation schemas con Joi + - 4 schemas: group, bet, user, friendRequest + - Middleware validation in `middleware/validation.js` + - Input sanitization automatico + +- [x] **Standardizzazione formato risposte API** + - ResponseHandler class in `shared/response.handler.js` + - Formato consistente per successo e errori + - Timestamp automatico in ogni risposta + - Metodi: success, created, error, badRequest, notFound, etc. + +- [x] **Error handling centralizzato** + - Custom errors: ValidationError, NotFoundError, ConflictError, etc. + - Global error handler middleware + - Gestione errori Sequelize e Mongoose + - Stack trace solo in development + +- [x] **Constants file per magic numbers/strings** + - `config/constants.js`: HTTP_STATUS, BET_STATUS, FRIEND_REQUEST_STATUS, etc. + - `config/database.js`: configurazione database centralizzata + - `config/index.js`: export centralizzato + +- [x] **Code linting (ESLint) e formatting (Prettier)** + - ESLint configurato con airbnb-base + - Prettier per code formatting + - Scripts npm: lint, lint:fix, format + - .eslintrc.json e .prettierrc + +- [ ] **Type checking con TypeScript** - SKIPPED (troppo invasivo per questa fase) + +### Nuova Struttura Architetturale + +``` +Routes (thin) → Validation → Controllers → Services → Repositories → Database + ↓ ↓ ↓ + ResponseHandler Business Logic DB Access +``` + +**Directory create:** +- `config/` - Configurazione centralizzata (3 files) +- `shared/errors/` - Custom errors (2 files) +- `shared/validators/schemas/` - Joi schemas (4 files) +- `shared/response.handler.js` - Standardized responses +- `middleware/` - errorHandler, validation, auth.mock (3 files) +- `di/` - Dependency injection container (1 file) +- `repositories/postgres/` - PostgreSQL repositories (4 files) +- `repositories/mongo/` - MongoDB repositories (1 file) +- `services/` - Business logic services (5 files) +- `controllers/` - Thin controllers (5 files) +- `routes/v1/` - API v1 routes (6 files) +- `routes/health.routes.js` - Health check endpoint + +**File modificati:** +- `server.js` - Refactored per integrare DI container e nuova architettura +- `package.json` - Aggiunte dipendenze e scripts + +### API Versioning + +- **`/api/v1/*`** - Nuova API con architettura refactored + - Formato risposte standardizzato + - Validazione automatica input + - Error handling consistente + +- **`/api/legacy/*`** - API legacy (backward compatibility) + - Routes originali spostate qui + - Mantiene formato originale + - Per garantire compatibilità durante migrazione + +- **`/health`** - Health check endpoint + - Verifica connessioni PostgreSQL e MongoDB + - Status: healthy/unhealthy + +### Endpoints API v1 + +#### Groups (`/api/v1/groups`) +- `GET /` - Lista tutti i gruppi +- `GET /:id` - Dettaglio gruppo con membri +- `POST /` - Crea gruppo (validato con Joi) +- `PUT /:id` - Aggiorna gruppo (validato) +- `DELETE /:id` - Elimina gruppo +- `PUT /:id/members` - Aggiungi membri (validato) +- `DELETE /:id/members/:userId` - Rimuovi membro + +#### Bets (`/api/v1/bets`) +- `GET /` - Lista tutte le scommesse +- `GET /:id` - Dettaglio scommessa +- `POST /` - Crea scommessa (validato con Joi) +- `PUT /:id` - Aggiorna scommessa (validato) +- `DELETE /:id` - Elimina scommessa + +#### Friend Requests (`/api/v1/friend-requests`) +- `GET /` - Lista amici dell'utente corrente +- `GET /pending` - Richieste pending +- `POST /` - Invia richiesta (validato con Joi) +- `POST /:id/accept` - Accetta richiesta +- `DELETE /:id` - Rifiuta/cancella richiesta + +#### Users (`/api/v1/users`) +- `PUT /current` - Aggiorna profilo utente corrente (validato) + +#### Initial Data (`/api/v1/initial-data`) +- `GET /` - Carica dati iniziali (currentUser, users, friends, groups, bets) + +#### Health Check (`/health`) +- `GET /` - Status database (PostgreSQL, MongoDB) + +### Formato Risposte Standardizzato + +**Successo:** +```json +{ + "success": true, + "message": "Operation successful", + "data": { ... }, + "timestamp": "2024-01-01T00:00:00.000Z" +} +``` + +**Errore:** +```json +{ + "success": false, + "message": "Error message", + "errors": [{"field": "name", "message": "Required"}], + "timestamp": "2024-01-01T00:00:00.000Z" +} +``` + +### Benefici dell'Architettura + +- ✅ **Separation of Concerns**: Ogni layer ha responsabilità ben definite +- ✅ **Testability**: Services e repositories facilmente testabili +- ✅ **Maintainability**: Business logic centralizzata +- ✅ **Consistency**: Formato risposte e errori standardizzati +- ✅ **Scalability**: Facile aggiungere nuove feature +- ✅ **Type Safety**: Validation layer previene dati inconsistenti +- ✅ **Backward Compatibility**: Routes legacy funzionano ancora + +--- + +## Note Tecniche + +### Dipendenze Principali +- **express**: ^4.19.2 +- **sequelize**: ^6.37.3 (PostgreSQL ORM) +- **mongoose**: ^8.4.0 (MongoDB ODM) +- **pg**: ^8.11.5 (PostgreSQL driver) +- **cors**: ^2.8.5 +- **awilix**: ^10.0.2 (Dependency Injection) +- **joi**: ^17.12.1 (Validation) + +### Dev Dependencies +- **eslint**: ^8.56.0 +- **prettier**: ^3.2.4 +- **eslint-config-airbnb-base**: ^15.0.0 +- **eslint-config-prettier**: ^9.1.0 + +### Variabili Ambiente Richieste +```bash +PORT=3000 +NODE_ENV=development +MONGO_URI=mongodb://root:root@servovio@localhost:27017/amici-scommesse +POSTGRES_URI=postgres://admin:admin@localhost:5432/amici_scommesse +``` + +### Porte +- Server: `3000` +- PostgreSQL: `5432` +- MongoDB: `27017` + +### Scripts npm +```bash +npm start # Avvia server +npm run lint # Check linting +npm run lint:fix # Fix linting automaticamente +npm run format # Format codice con Prettier +``` diff --git a/backend-node/DOCKER_GUIDE.md b/backend-node/DOCKER_GUIDE.md new file mode 100644 index 0000000..a263bc5 --- /dev/null +++ b/backend-node/DOCKER_GUIDE.md @@ -0,0 +1,433 @@ +# 🐳 Docker Guide - Amici Scommesse Backend + +Guida completa per eseguire il backend con Docker e Docker Compose. + +## 📋 Prerequisiti + +- Docker >= 20.10 +- Docker Compose >= 2.0 + +Verifica installazione: +```bash +docker --version +docker compose version +``` + +## 🚀 Quick Start + +### Development (con Hot Reload) + +```bash +# Avvia tutti i servizi in development +docker compose -f docker-compose.dev.yml up -d + +# Visualizza logs +docker compose -f docker-compose.dev.yml logs -f backend + +# Ferma tutti i servizi +docker compose -f docker-compose.dev.yml down +``` + +### Production + +```bash +# Avvia tutti i servizi in production +docker compose up -d + +# Visualizza logs +docker compose logs -f backend + +# Ferma tutti i servizi +docker compose down +``` + +## 📦 Servizi Inclusi + +### Docker Compose Production (`docker-compose.yml`) + +| Servizio | Porta | Descrizione | +|----------|-------|-------------| +| `backend` | 3000 | Backend Node.js (production build) | +| `postgres` | 5432 | PostgreSQL 16 | +| `mongodb` | 27017 | MongoDB 7 | + +### Docker Compose Development (`docker-compose.dev.yml`) + +| Servizio | Porta | Descrizione | +|----------|-------|-------------| +| `backend` | 3000 | Backend Node.js (development + hot reload) | +| `postgres` | 5432 | PostgreSQL 16 | +| `mongodb` | 27017 | MongoDB 7 | +| `mongo-express` | 8081 | Web UI per MongoDB (opzionale) | +| `pgadmin` | 5050 | Web UI per PostgreSQL (opzionale) | + +## 🔧 Configurazione + +### File .env + +Crea un file `.env` nella root del progetto: + +```bash +# Copia il template +cp .env.example .env + +# Modifica le variabili se necessario +nano .env +``` + +Le variabili d'ambiente sono automaticamente caricate da Docker Compose. + +### Variabili Importanti + +```env +# Application +NODE_ENV=development +PORT=3000 + +# PostgreSQL +POSTGRES_DB=amici_scommesse +POSTGRES_USER=admin +POSTGRES_PASSWORD=admin + +# MongoDB +MONGO_DB=amici-scommesse +MONGO_USER=root +MONGO_PASSWORD=root@servovio +``` + +## 📝 Comandi Docker Compose + +### Gestione Servizi + +```bash +# Avvia tutti i servizi (production) +docker compose up -d + +# Avvia tutti i servizi (development) +docker compose -f docker-compose.dev.yml up -d + +# Avvia solo alcuni servizi +docker compose up -d postgres mongodb + +# Ferma tutti i servizi +docker compose down + +# Ferma e rimuovi volumi (ATTENZIONE: cancella i dati!) +docker compose down -v + +# Restart di un servizio specifico +docker compose restart backend +``` + +### Build e Rebuild + +```bash +# Build dell'immagine backend +docker compose build backend + +# Rebuild forzato (ignora cache) +docker compose build --no-cache backend + +# Build e avvia +docker compose up -d --build +``` + +### Logs e Debug + +```bash +# Visualizza logs di tutti i servizi +docker compose logs + +# Visualizza logs di un servizio specifico +docker compose logs backend + +# Follow logs in real-time +docker compose logs -f backend + +# Ultimi 100 log lines +docker compose logs --tail=100 backend + +# Logs da timestamp specifico +docker compose logs --since 2024-01-01T00:00:00 +``` + +### Shell nei Container + +```bash +# Shell nel container backend +docker compose exec backend sh + +# Shell nel container postgres +docker compose exec postgres psql -U admin -d amici_scommesse + +# Shell nel container mongodb +docker compose exec mongodb mongosh -u root -p root@servovio --authenticationDatabase admin +``` + +### Health Check + +```bash +# Verifica health di tutti i servizi +docker compose ps + +# Health check specifico +curl http://localhost:3000/health +``` + +## 🔍 Troubleshooting + +### Il backend non si avvia + +**1. Verifica che i database siano healthy:** +```bash +docker compose ps +``` + +Dovresti vedere: +``` +NAME STATUS HEALTH +amici-scommesse-backend Up 30 seconds healthy +amici-scommesse-postgres Up 45 seconds (healthy) healthy +amici-scommesse-mongodb Up 45 seconds (healthy) healthy +``` + +**2. Controlla i logs:** +```bash +docker compose logs backend +``` + +**3. Verifica la connessione ai database:** +```bash +# Test PostgreSQL +docker compose exec postgres pg_isready -U admin + +# Test MongoDB +docker compose exec mongodb mongosh --eval "db.adminCommand('ping')" +``` + +### Errore "port already in use" + +**Soluzione**: Cambia la porta nel file `.env`: +```env +PORT=3001 +POSTGRES_PORT=5433 +MONGO_PORT=27018 +``` + +### I database non mantengono i dati + +**Soluzione**: Verifica che i volumi esistano: +```bash +docker volume ls | grep amici-scommesse +``` + +Per ripristinare i volumi: +```bash +docker compose down +docker compose up -d +``` + +### Hot reload non funziona in development + +**Verifica volumi**: Il file `docker-compose.dev.yml` deve avere: +```yaml +volumes: + - ./:/app + - /app/node_modules +``` + +**Rebuild del container**: +```bash +docker compose -f docker-compose.dev.yml up -d --build +``` + +### Errore di connessione tra servizi + +**Soluzione**: Usa i nomi dei servizi come hostname: +- Backend → Postgres: `postgres:5432` +- Backend → MongoDB: `mongodb:27017` + +Non usare `localhost` quando sei dentro Docker! + +## 🌐 Accesso ai Servizi + +### Backend API + +- **API v1**: http://localhost:3000/api/v1 +- **Legacy API**: http://localhost:3000/api/legacy +- **Health Check**: http://localhost:3000/health + +### Database Web UIs (Solo Development) + +**Mongo Express** (MongoDB UI): +- URL: http://localhost:8081 +- Username: `admin` +- Password: `admin` + +**pgAdmin** (PostgreSQL UI): +- URL: http://localhost:5050 +- Email: `admin@admin.com` +- Password: `admin` + +**Connessione PostgreSQL in pgAdmin**: +1. Add New Server +2. General → Name: `Amici Scommesse` +3. Connection: + - Host: `postgres` (il nome del servizio Docker) + - Port: `5432` + - Username: `admin` + - Password: `admin` + - Database: `amici_scommesse` + +## 📊 Gestione Dati + +### Backup Database + +**PostgreSQL**: +```bash +# Backup +docker compose exec postgres pg_dump -U admin amici_scommesse > backup.sql + +# Restore +docker compose exec -T postgres psql -U admin amici_scommesse < backup.sql +``` + +**MongoDB**: +```bash +# Backup +docker compose exec mongodb mongodump --username root --password root@servovio --authenticationDatabase admin --db amici-scommesse --out /tmp/backup + +# Export dal container +docker compose cp mongodb:/tmp/backup ./mongodb-backup + +# Restore +docker compose cp ./mongodb-backup mongodb:/tmp/backup +docker compose exec mongodb mongorestore --username root --password root@servovio --authenticationDatabase admin --db amici-scommesse /tmp/backup/amici-scommesse +``` + +### Reset Completo + +**ATTENZIONE: Cancella tutti i dati!** + +```bash +# Ferma tutto e rimuovi volumi +docker compose down -v + +# Rimuovi immagini +docker compose rm -f + +# Rebuild e riavvia +docker compose up -d --build +``` + +## 🎯 Best Practices + +### Development + +1. **Usa docker-compose.dev.yml** per development +2. **Mount dei volumi** per hot reload +3. **Abilita SQL logging** per debug +4. **Usa le Web UI** per ispezionare i database + +### Production + +1. **Usa docker-compose.yml** per production +2. **Non montare volumi sorgente** +3. **Disabilita SQL logging** +4. **Usa secrets** per password sensibili +5. **Configura backup automatici** + +### Sicurezza + +1. **Non committare .env** nel repository +2. **Cambia password** di default per production +3. **Usa user non-root** (già configurato nel Dockerfile) +4. **Limita risorse** del container se necessario + +## 🔨 Multi-Stage Build + +Il Dockerfile usa multi-stage build per ottimizzare le immagini: + +| Stage | Target | Uso | +|-------|--------|-----| +| `base` | - | Base comune | +| `dependencies` | - | Tutte le dipendenze | +| `development` | Development | Con devDependencies + hot reload | +| `production-dependencies` | - | Solo prod dependencies | +| `production` | Production | Ottimizzato, user non-root | + +**Build target specifico**: +```bash +# Development +docker build --target development -t backend:dev . + +# Production +docker build --target production -t backend:prod . +``` + +## 📈 Monitoraggio + +### Resource Usage + +```bash +# Uso risorse di tutti i container +docker stats + +# Uso risorse di un container specifico +docker stats amici-scommesse-backend +``` + +### Container Logs in Produzione + +Per produzione, considera l'uso di: +- **Docker logging drivers** (json-file, syslog, fluentd) +- **Log aggregators** (ELK Stack, Loki) +- **APM tools** (Datadog, New Relic) + +## 🚀 Deploy in Production + +### Docker Swarm + +```bash +# Inizializza Swarm +docker swarm init + +# Deploy lo stack +docker stack deploy -c docker-compose.yml amici-scommesse + +# Lista servizi +docker service ls + +# Scala il backend +docker service scale amici-scommesse_backend=3 +``` + +### Kubernetes + +Per Kubernetes, converti il docker-compose: +```bash +# Installa kompose +curl -L https://github.com/kubernetes/kompose/releases/download/v1.31.2/kompose-linux-amd64 -o kompose +chmod +x kompose +sudo mv kompose /usr/local/bin + +# Converti +kompose convert -f docker-compose.yml +``` + +## 🔗 Link Utili + +- [Docker Documentation](https://docs.docker.com/) +- [Docker Compose Documentation](https://docs.docker.com/compose/) +- [Docker Best Practices](https://docs.docker.com/develop/dev-best-practices/) +- [PostgreSQL Docker Image](https://hub.docker.com/_/postgres) +- [MongoDB Docker Image](https://hub.docker.com/_/mongo) + +## 💡 Tips + +1. **Cache delle dipendenze**: `COPY package*.json` prima di `COPY .` per ottimizzare il build +2. **Multi-stage**: Usa stage diversi per dev e prod +3. **Health checks**: Sempre configurare healthcheck per monitoraggio +4. **Named volumes**: Usa volumi nominati per persistenza dati +5. **Networks**: Isola i servizi in network dedicati +6. **Secrets**: Usa Docker secrets per dati sensibili in produzione diff --git a/backend-node/Dockerfile b/backend-node/Dockerfile new file mode 100644 index 0000000..136db39 --- /dev/null +++ b/backend-node/Dockerfile @@ -0,0 +1,89 @@ +# ======================================== +# Multi-stage Dockerfile for Backend Node.js +# ======================================== + +# ======================================== +# Stage 1: Base +# ======================================== +FROM node:20-alpine AS base + +# Install dumb-init for proper signal handling +RUN apk add --no-cache dumb-init + +# Set working directory +WORKDIR /app + +# Copy package files for dependency installation +COPY package*.json ./ + +# ======================================== +# Stage 2: Dependencies +# ======================================== +FROM base AS dependencies + +# Install ALL dependencies (including devDependencies for build) +RUN npm ci + +# ======================================== +# Stage 3: Development +# ======================================== +FROM dependencies AS development + +# Set NODE_ENV +ENV NODE_ENV=development + +# Copy application code +COPY . . + +# Expose port +EXPOSE 3000 + +# Use dumb-init to handle signals properly +ENTRYPOINT ["dumb-init", "--"] + +# Start with nodemon for hot reload +CMD ["npm", "start"] + +# ======================================== +# Stage 4: Production Dependencies +# ======================================== +FROM base AS production-dependencies + +# Install only production dependencies +RUN npm ci --only=production && \ + npm cache clean --force + +# ======================================== +# Stage 5: Production Build +# ======================================== +FROM base AS production + +# Set NODE_ENV +ENV NODE_ENV=production + +# Copy production dependencies +COPY --from=production-dependencies /app/node_modules ./node_modules + +# Copy application code +COPY . . + +# Create non-root user for security +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nodejs -u 1001 && \ + chown -R nodejs:nodejs /app + +# Switch to non-root user +USER nodejs + +# Expose port +EXPOSE 3000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ + CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})" + +# Use dumb-init to handle signals properly +ENTRYPOINT ["dumb-init", "--"] + +# Start the application +CMD ["node", "server.js"] diff --git a/backend-node/IMPLEMENTATION_GUIDE.md b/backend-node/IMPLEMENTATION_GUIDE.md new file mode 100644 index 0000000..96c9e9c --- /dev/null +++ b/backend-node/IMPLEMENTATION_GUIDE.md @@ -0,0 +1,277 @@ +# Guida all'Implementazione - Miglioramenti Tecnici + +## ✅ Implementazione Completata + +Tutti i miglioramenti tecnici proposti nel CHANGELOG.md sono stati implementati: + +1. ✅ Separazione logica business dai controller (service layer) +2. ✅ Dependency injection container (Awilix) +3. ✅ Request/Response DTOs (Joi validation) +4. ✅ Standardizzazione formato risposte API +5. ✅ Error handling centralizzato +6. ✅ Constants file per magic numbers/strings +7. ⏭️ Type checking con TypeScript - SKIPPED (come pianificato) +8. ✅ Code linting (ESLint) e formatting (Prettier) + +## 🚀 Prossimi Passi per Avviare il Backend + +### 1. Installare le Dipendenze + +```bash +cd /home/claude/scomettiapp/backend-node + +# Installare dipendenze di produzione +npm install awilix@^10.0.2 joi@^17.12.1 + +# Installare dipendenze di sviluppo +npm install --save-dev eslint@^8.56.0 eslint-config-airbnb-base@^15.0.0 eslint-plugin-import@^2.29.1 prettier@^3.2.4 eslint-config-prettier@^9.1.0 eslint-plugin-prettier@^5.1.3 +``` + +### 2. Verificare Linting + +```bash +# Controllare errori di linting +npm run lint + +# Fixare automaticamente errori di linting +npm run lint:fix + +# Formattare tutto il codice +npm run format +``` + +### 3. Avviare il Server + +```bash +npm start +``` + +Output atteso: +``` +Connected to MongoDB +Connected to PostgreSQL +PostgreSQL models synchronized +Server is running on http://localhost:3000 +Environment: development +API v1 endpoints available at /api/v1/ +Legacy endpoints available at /api/legacy/ +Health check available at /health +``` + +## 🧪 Testing dell'Implementazione + +### Test 1: Health Check + +```bash +curl http://localhost:3000/health +``` + +Risposta attesa: +```json +{ + "status": "healthy", + "timestamp": "2024-01-01T00:00:00.000Z", + "services": { + "postgres": "connected", + "mongodb": "connected" + } +} +``` + +### Test 2: Creare un Gruppo (v1 API) + +```bash +curl -X POST http://localhost:3000/api/v1/groups \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Test Group", + "members": [1, 2] + }' +``` + +Risposta attesa (formato standardizzato): +```json +{ + "success": true, + "message": "Group created successfully", + "data": { + "id": 3, + "name": "Test Group", + "members": [...] + }, + "timestamp": "2024-01-01T00:00:00.000Z" +} +``` + +### Test 3: Validazione Errore + +```bash +curl -X POST http://localhost:3000/api/v1/groups \ + -H "Content-Type: application/json" \ + -d '{ + "name": "", + "members": [] + }' +``` + +Risposta attesa (errore standardizzato): +```json +{ + "success": false, + "message": "Validation failed", + "errors": [ + {"field": "name", "message": "Group name is required"}, + {"field": "members", "message": "At least one member is required"} + ], + "timestamp": "2024-01-01T00:00:00.000Z" +} +``` + +### Test 4: Not Found Error + +```bash +curl http://localhost:3000/api/v1/groups/999 +``` + +Risposta attesa: +```json +{ + "success": false, + "message": "Group not found", + "timestamp": "2024-01-01T00:00:00.000Z" +} +``` + +### Test 5: Verificare Compatibilità Legacy + +```bash +# Legacy route (vecchio formato, dovrebbe ancora funzionare) +curl http://localhost:3000/api/legacy/groups +``` + +### Test 6: Initial Data + +```bash +curl http://localhost:3000/api/v1/initial-data +``` + +Risposta attesa: +```json +{ + "success": true, + "message": "Initial data retrieved successfully", + "data": { + "currentUser": {...}, + "users": [...], + "friends": [...], + "friendRequests": [...], + "groups": [...], + "userBets": [...], + "publicBets": [...] + }, + "timestamp": "2024-01-01T00:00:00.000Z" +} +``` + +## 📋 Checklist Verifica Completa + +- [ ] Server si avvia senza errori +- [ ] PostgreSQL connesso +- [ ] MongoDB connesso +- [ ] Health check risponde correttamente +- [ ] Routes v1 rispondono con formato standardizzato +- [ ] Validation errors catturati e formattati +- [ ] Not found errors ritornano 404 consistente +- [ ] Routes legacy funzionano ancora (backward compatibility) +- [ ] ESLint passa senza errori critici +- [ ] Nessuna logica business nei controllers (solo nei services) + +## 🎯 Endpoints API v1 + +### Groups +- `GET /api/v1/groups` - Lista tutti i gruppi +- `GET /api/v1/groups/:id` - Dettaglio gruppo +- `POST /api/v1/groups` - Crea gruppo (validato) +- `PUT /api/v1/groups/:id` - Aggiorna gruppo (validato) +- `DELETE /api/v1/groups/:id` - Elimina gruppo +- `PUT /api/v1/groups/:id/members` - Aggiungi membri (validato) +- `DELETE /api/v1/groups/:id/members/:userId` - Rimuovi membro + +### Bets +- `GET /api/v1/bets` - Lista tutte le scommesse +- `GET /api/v1/bets/:id` - Dettaglio scommessa +- `POST /api/v1/bets` - Crea scommessa (validato) +- `PUT /api/v1/bets/:id` - Aggiorna scommessa (validato) +- `DELETE /api/v1/bets/:id` - Elimina scommessa + +### Friend Requests +- `GET /api/v1/friend-requests` - Lista amici +- `GET /api/v1/friend-requests/pending` - Richieste pending +- `POST /api/v1/friend-requests` - Invia richiesta (validato) +- `POST /api/v1/friend-requests/:id/accept` - Accetta richiesta +- `DELETE /api/v1/friend-requests/:id` - Rifiuta richiesta + +### Users +- `PUT /api/v1/users/current` - Aggiorna profilo (validato) + +### Initial Data +- `GET /api/v1/initial-data` - Carica dati iniziali + +### Health +- `GET /health` - Status database + +## 🏗️ Architettura Implementata + +``` +Request → Routes → Validation → Controller → Service → Repository → Database + ↓ ↓ + Joi Schema Business Logic + ↓ + Custom Errors (ValidationError, etc.) + ↓ + Global Error Handler Middleware + ↓ + Standardized Response (ResponseHandler) +``` + +## 📊 Benefici dell'Architettura + +1. **Separation of Concerns**: Ogni layer ha responsabilità ben definite +2. **Testability**: Services e repositories facilmente testabili con mock +3. **Maintainability**: Business logic centralizzata +4. **Consistency**: Formato risposte e errori standardizzati +5. **Scalability**: Facile aggiungere nuove feature +6. **Type Safety**: Validation layer previene dati inconsistenti +7. **Backward Compatibility**: Routes legacy funzionano ancora +8. **Flexibility**: DI container permette sostituzioni facili + +## 🔧 Troubleshooting + +### Errore: "Cannot find module 'awilix'" +Eseguire `npm install` per installare tutte le dipendenze. + +### Errore: Database connection failed +Verificare che PostgreSQL e MongoDB siano in esecuzione e accessibili. + +### Errore: ESLint errors +Eseguire `npm run lint:fix` per correggere automaticamente i problemi di formattazione. + +### Routes legacy non funzionano +Le routes legacy sono state spostate su `/api/legacy/*` per mantenere compatibilità. + +## 📝 Note Importanti + +- **TypeScript**: Non implementato in questa fase (come pianificato) +- **Testing automatici**: Da implementare in fase successiva +- **Logging strutturato**: Opzionale, non implementato +- **Production**: Richiede ulteriori configurazioni (migrations, real auth, etc.) + +## 🎉 Prossimi Step Consigliati + +1. Installare dipendenze con npm +2. Testare tutti gli endpoint v1 +3. Migrare frontend da `/api/*` a `/api/v1/*` +4. Eventualmente deprecare `/api/legacy/*` +5. Aggiungere test automatici (Jest) +6. Implementare logging strutturato (Winston) +7. Considerare migrazione a TypeScript in futuro diff --git a/backend-node/config/constants.js b/backend-node/config/constants.js new file mode 100644 index 0000000..771d6e7 --- /dev/null +++ b/backend-node/config/constants.js @@ -0,0 +1,46 @@ +module.exports = { + // HTTP Status Codes + HTTP_STATUS: { + OK: 200, + CREATED: 201, + BAD_REQUEST: 400, + UNAUTHORIZED: 401, + FORBIDDEN: 403, + NOT_FOUND: 404, + CONFLICT: 409, + INTERNAL_SERVER_ERROR: 500, + }, + + // Bet Status + BET_STATUS: { + ACTIVE: 'ACTIVE', + AWAITING_VALIDATION: 'AWAITING_VALIDATION', + COMPLETED: 'COMPLETED', + EXPIRED: 'EXPIRED', + }, + + // Friend Request Status + FRIEND_REQUEST_STATUS: { + PENDING: 'PENDING', + ACCEPTED: 'ACCEPTED', + REJECTED: 'REJECTED', + }, + + // Participant Roles + PARTICIPANT_ROLES: { + PARTICIPANT: 'PARTICIPANT', + OBSERVER: 'OBSERVER', + }, + + // Pagination Defaults + PAGINATION: { + DEFAULT_PAGE: 1, + DEFAULT_LIMIT: 20, + MAX_LIMIT: 100, + }, + + // Mock Auth (temporaneo) + MOCK_AUTH: { + DEFAULT_USER_NAME: 'Mario Rossi', + }, +}; diff --git a/backend-node/config/database.js b/backend-node/config/database.js new file mode 100644 index 0000000..7309dce --- /dev/null +++ b/backend-node/config/database.js @@ -0,0 +1,28 @@ +module.exports = { + postgres: { + uri: + process.env.POSTGRES_URI || + 'postgres://admin:admin@localhost:5432/amici_scommesse', + options: { + dialect: 'postgres', + logging: process.env.NODE_ENV === 'development' ? console.log : false, + pool: { + max: 10, + min: 0, + acquire: 30000, + idle: 10000, + }, + }, + }, + + mongo: { + uri: + process.env.MONGO_URI || + 'mongodb://localhost:27017/amici-scommesse', + options: { + user: 'root', + pass: 'root@servovio', + authSource: 'admin', + }, + }, +}; diff --git a/backend-node/config/index.js b/backend-node/config/index.js new file mode 100644 index 0000000..1027bc5 --- /dev/null +++ b/backend-node/config/index.js @@ -0,0 +1,11 @@ +const constants = require('./constants'); +const database = require('./database'); + +module.exports = { + port: process.env.PORT || 3000, + nodeEnv: process.env.NODE_ENV || 'development', + isDevelopment: process.env.NODE_ENV === 'development', + isProduction: process.env.NODE_ENV === 'production', + ...constants, + database, +}; diff --git a/backend-node/controllers/bets.controller.js b/backend-node/controllers/bets.controller.js new file mode 100644 index 0000000..03b9114 --- /dev/null +++ b/backend-node/controllers/bets.controller.js @@ -0,0 +1,54 @@ +const ResponseHandler = require('../shared/response.handler'); + +class BetsController { + constructor({ betsService }) { + this.betsService = betsService; + } + + async getAllBets(req, res, next) { + try { + const bets = await this.betsService.getAllBets(); + return ResponseHandler.success(res, bets, 'Bets retrieved successfully'); + } catch (error) { + next(error); + } + } + + async getBetById(req, res, next) { + try { + const bet = await this.betsService.getBetById(req.params.id); + return ResponseHandler.success(res, bet, 'Bet retrieved successfully'); + } catch (error) { + next(error); + } + } + + async createBet(req, res, next) { + try { + const bet = await this.betsService.createBet(req.body); + return ResponseHandler.created(res, bet, 'Bet created successfully'); + } catch (error) { + next(error); + } + } + + async updateBet(req, res, next) { + try { + const bet = await this.betsService.updateBet(req.params.id, req.body); + return ResponseHandler.success(res, bet, 'Bet updated successfully'); + } catch (error) { + next(error); + } + } + + async deleteBet(req, res, next) { + try { + await this.betsService.deleteBet(req.params.id); + return ResponseHandler.success(res, null, 'Bet deleted successfully'); + } catch (error) { + next(error); + } + } +} + +module.exports = BetsController; diff --git a/backend-node/controllers/friends.controller.js b/backend-node/controllers/friends.controller.js new file mode 100644 index 0000000..8a53ae1 --- /dev/null +++ b/backend-node/controllers/friends.controller.js @@ -0,0 +1,67 @@ +const ResponseHandler = require('../shared/response.handler'); + +class FriendsController { + constructor({ friendsService }) { + this.friendsService = friendsService; + } + + async getFriends(req, res, next) { + try { + const friends = await this.friendsService.getFriendsByUserId(req.userId); + return ResponseHandler.success(res, friends, 'Friends retrieved successfully'); + } catch (error) { + next(error); + } + } + + async getPendingRequests(req, res, next) { + try { + const requests = await this.friendsService.getPendingRequests(req.userId); + return ResponseHandler.success( + res, + requests, + 'Pending requests retrieved successfully' + ); + } catch (error) { + next(error); + } + } + + async sendRequest(req, res, next) { + try { + const request = await this.friendsService.sendFriendRequest( + req.userId, + req.body.identifier + ); + return ResponseHandler.created(res, request, 'Friend request sent successfully'); + } catch (error) { + next(error); + } + } + + async acceptRequest(req, res, next) { + try { + const result = await this.friendsService.acceptFriendRequest( + req.params.id, + req.userId + ); + return ResponseHandler.success(res, result, result.message); + } catch (error) { + next(error); + } + } + + async rejectRequest(req, res, next) { + try { + const result = await this.friendsService.rejectFriendRequest( + req.params.id, + req.userId + ); + return ResponseHandler.success(res, result, result.message); + } catch (error) { + next(error); + } + } +} + +module.exports = FriendsController; diff --git a/backend-node/controllers/groups.controller.js b/backend-node/controllers/groups.controller.js new file mode 100644 index 0000000..c96f5f2 --- /dev/null +++ b/backend-node/controllers/groups.controller.js @@ -0,0 +1,78 @@ +const ResponseHandler = require('../shared/response.handler'); + +class GroupsController { + constructor({ groupsService }) { + this.groupsService = groupsService; + } + + async getAllGroups(req, res, next) { + try { + const groups = await this.groupsService.getAllGroups(); + return ResponseHandler.success(res, groups, 'Groups retrieved successfully'); + } catch (error) { + next(error); + } + } + + async getGroupById(req, res, next) { + try { + const group = await this.groupsService.getGroupById(req.params.id); + return ResponseHandler.success(res, group, 'Group retrieved successfully'); + } catch (error) { + next(error); + } + } + + async createGroup(req, res, next) { + try { + const group = await this.groupsService.createGroup(req.body); + return ResponseHandler.created(res, group, 'Group created successfully'); + } catch (error) { + next(error); + } + } + + async updateGroup(req, res, next) { + try { + const group = await this.groupsService.updateGroup(req.params.id, req.body); + return ResponseHandler.success(res, group, 'Group updated successfully'); + } catch (error) { + next(error); + } + } + + async deleteGroup(req, res, next) { + try { + await this.groupsService.deleteGroup(req.params.id); + return ResponseHandler.success(res, null, 'Group deleted successfully'); + } catch (error) { + next(error); + } + } + + async addMembers(req, res, next) { + try { + const group = await this.groupsService.addMembersToGroup( + req.params.id, + req.body.userIds + ); + return ResponseHandler.success(res, group, 'Members added successfully'); + } catch (error) { + next(error); + } + } + + async removeMember(req, res, next) { + try { + const group = await this.groupsService.removeMemberFromGroup( + req.params.id, + req.params.userId + ); + return ResponseHandler.success(res, group, 'Member removed successfully'); + } catch (error) { + next(error); + } + } +} + +module.exports = GroupsController; diff --git a/backend-node/controllers/initialData.controller.js b/backend-node/controllers/initialData.controller.js new file mode 100644 index 0000000..3138e9b --- /dev/null +++ b/backend-node/controllers/initialData.controller.js @@ -0,0 +1,38 @@ +const ResponseHandler = require('../shared/response.handler'); + +class InitialDataController { + constructor({ usersService, friendsService, groupsService, betsService }) { + this.usersService = usersService; + this.friendsService = friendsService; + this.groupsService = groupsService; + this.betsService = betsService; + } + + async getInitialData(req, res, next) { + try { + const currentUser = await this.usersService.getUserById(req.userId); + const users = await this.usersService.getAllUsers(); + const friends = await this.friendsService.getFriendsByUserId(req.userId); + const friendRequests = await this.friendsService.getPendingRequests(req.userId); + const groups = await this.groupsService.getAllGroups(); + const userBets = await this.betsService.getUserBets(req.userId); + const publicBets = await this.betsService.getPublicBetsExcludingUser(req.userId); + + const data = { + currentUser, + users, + friends, + friendRequests, + groups, + userBets, + publicBets, + }; + + return ResponseHandler.success(res, data, 'Initial data retrieved successfully'); + } catch (error) { + next(error); + } + } +} + +module.exports = InitialDataController; diff --git a/backend-node/controllers/users.controller.js b/backend-node/controllers/users.controller.js new file mode 100644 index 0000000..1a0e72e --- /dev/null +++ b/backend-node/controllers/users.controller.js @@ -0,0 +1,18 @@ +const ResponseHandler = require('../shared/response.handler'); + +class UsersController { + constructor({ usersService }) { + this.usersService = usersService; + } + + async updateCurrentUser(req, res, next) { + try { + const user = await this.usersService.updateUserProfile(req.userId, req.body); + return ResponseHandler.success(res, user, 'User profile updated successfully'); + } catch (error) { + next(error); + } + } +} + +module.exports = UsersController; diff --git a/backend-node/di/container.js b/backend-node/di/container.js new file mode 100644 index 0000000..cdb78cb --- /dev/null +++ b/backend-node/di/container.js @@ -0,0 +1,81 @@ +const { createContainer, asClass, asFunction, asValue } = require('awilix'); +const { Sequelize } = require('sequelize'); +const mongoose = require('mongoose'); +const fs = require('fs'); +const path = require('path'); +const config = require('../config'); + +function setupContainer() { + const container = createContainer(); + + container.register({ + sequelize: asFunction(() => { + const sequelize = new Sequelize( + config.database.postgres.uri, + config.database.postgres.options + ); + + const modelsDir = path.join(__dirname, '../models'); + fs.readdirSync(modelsDir) + .filter( + (file) => + file.indexOf('.') !== 0 && + file.slice(-3) === '.js' && + file !== 'bet.model.js' && + file !== 'associations.js' + ) + .forEach((file) => { + require(path.join(modelsDir, file))(sequelize); + }); + + const defineAssociations = require('../models/associations'); + defineAssociations(sequelize); + + return sequelize; + }).singleton(), + + mongoose: asValue(mongoose), + }); + + container.register({ + usersRepository: asClass( + require('../repositories/postgres/users.repository') + ).scoped(), + groupsRepository: asClass( + require('../repositories/postgres/groups.repository') + ).scoped(), + friendshipsRepository: asClass( + require('../repositories/postgres/friendships.repository') + ).scoped(), + friendRequestsRepository: asClass( + require('../repositories/postgres/friendRequests.repository') + ).scoped(), + betsRepository: asClass( + require('../repositories/mongo/bets.repository') + ).scoped(), + }); + + container.register({ + dataStitchingService: asClass( + require('../services/data-stitching.service') + ).scoped(), + usersService: asClass(require('../services/users.service')).scoped(), + groupsService: asClass(require('../services/groups.service')).scoped(), + friendsService: asClass(require('../services/friends.service')).scoped(), + betsService: asClass(require('../services/bets.service')).scoped(), + }); + + container.register({ + betsController: asClass(require('../controllers/bets.controller')).scoped(), + groupsController: asClass(require('../controllers/groups.controller')).scoped(), + usersController: asClass(require('../controllers/users.controller')).scoped(), + friendsController: asClass(require('../controllers/friends.controller')).scoped(), + initialDataController: asClass( + require('../controllers/initialData.controller') + ).scoped(), + }); + + return container; +} + +module.exports = setupContainer; diff --git a/backend-node/docker-compose.dev.yml b/backend-node/docker-compose.dev.yml new file mode 100644 index 0000000..a2e8b13 --- /dev/null +++ b/backend-node/docker-compose.dev.yml @@ -0,0 +1,170 @@ +version: '3.8' + +services: + # ======================================== + # PostgreSQL Database (Development) + # ======================================== + postgres: + image: postgres:16-alpine + container_name: amici-scommesse-postgres-dev + restart: unless-stopped + environment: + POSTGRES_DB: ${POSTGRES_DB:-amici_scommesse} + POSTGRES_USER: ${POSTGRES_USER:-admin} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-admin} + PGDATA: /var/lib/postgresql/data/pgdata + ports: + - "${POSTGRES_PORT:-5432}:5432" + volumes: + - postgres_data_dev:/var/lib/postgresql/data + - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-admin} -d ${POSTGRES_DB:-amici_scommesse}"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - amici-scommesse-network-dev + + # ======================================== + # MongoDB Database (Development) + # ======================================== + mongodb: + image: mongo:7-jammy + container_name: amici-scommesse-mongodb-dev + restart: unless-stopped + environment: + MONGO_INITDB_ROOT_USERNAME: ${MONGO_USER:-root} + MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD:-root@servovio} + MONGO_INITDB_DATABASE: ${MONGO_DB:-amici-scommesse} + ports: + - "${MONGO_PORT:-27017}:27017" + volumes: + - mongodb_data_dev:/data/db + - mongodb_config_dev:/data/configdb + healthcheck: + test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - amici-scommesse-network-dev + + # ======================================== + # Backend Node.js (Development with Hot Reload) + # ======================================== + backend: + build: + context: . + dockerfile: Dockerfile + target: development + container_name: amici-scommesse-backend-dev + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + mongodb: + condition: service_healthy + environment: + NODE_ENV: development + PORT: ${PORT:-3000} + + # PostgreSQL connection + POSTGRES_HOST: postgres + POSTGRES_PORT: 5432 + POSTGRES_DB: ${POSTGRES_DB:-amici_scommesse} + POSTGRES_USER: ${POSTGRES_USER:-admin} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-admin} + POSTGRES_URI: postgres://${POSTGRES_USER:-admin}:${POSTGRES_PASSWORD:-admin}@postgres:5432/${POSTGRES_DB:-amici_scommesse} + + # MongoDB connection + MONGO_HOST: mongodb + MONGO_PORT: 27017 + MONGO_DB: ${MONGO_DB:-amici-scommesse} + MONGO_USER: ${MONGO_USER:-root} + MONGO_PASSWORD: ${MONGO_PASSWORD:-root@servovio} + MONGO_AUTH_SOURCE: ${MONGO_AUTH_SOURCE:-admin} + MONGO_URI: mongodb://${MONGO_USER:-root}:${MONGO_PASSWORD:-root@servovio}@mongodb:27017/${MONGO_DB:-amici-scommesse}?authSource=${MONGO_AUTH_SOURCE:-admin} + + # Development config + LOG_LEVEL: debug + ENABLE_SQL_LOGGING: true + CORS_ORIGIN: ${CORS_ORIGIN:-http://localhost:5173} + MOCK_USER_NAME: ${MOCK_USER_NAME:-Mario Rossi} + DB_SYNC_FORCE: true + ports: + - "${PORT:-3000}:3000" + volumes: + # Mount source code for hot reload + - ./:/app + - /app/node_modules + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"] + interval: 30s + timeout: 10s + start_period: 40s + retries: 3 + networks: + - amici-scommesse-network-dev + + # ======================================== + # Mongo Express (Optional - Web UI for MongoDB) + # ======================================== + mongo-express: + image: mongo-express:latest + container_name: amici-scommesse-mongo-express + restart: unless-stopped + depends_on: + - mongodb + environment: + ME_CONFIG_MONGODB_ADMINUSERNAME: ${MONGO_USER:-root} + ME_CONFIG_MONGODB_ADMINPASSWORD: ${MONGO_PASSWORD:-root@servovio} + ME_CONFIG_MONGODB_URL: mongodb://${MONGO_USER:-root}:${MONGO_PASSWORD:-root@servovio}@mongodb:27017/ + ME_CONFIG_BASICAUTH_USERNAME: admin + ME_CONFIG_BASICAUTH_PASSWORD: admin + ports: + - "8081:8081" + networks: + - amici-scommesse-network-dev + + # ======================================== + # pgAdmin (Optional - Web UI for PostgreSQL) + # ======================================== + pgadmin: + image: dpage/pgadmin4:latest + container_name: amici-scommesse-pgadmin + restart: unless-stopped + depends_on: + - postgres + environment: + PGADMIN_DEFAULT_EMAIL: admin@admin.com + PGADMIN_DEFAULT_PASSWORD: admin + PGADMIN_CONFIG_SERVER_MODE: 'False' + PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False' + ports: + - "5050:80" + volumes: + - pgadmin_data_dev:/var/lib/pgadmin + networks: + - amici-scommesse-network-dev + +# ======================================== +# Networks +# ======================================== +networks: + amici-scommesse-network-dev: + driver: bridge + name: amici-scommesse-network-dev + +# ======================================== +# Volumes +# ======================================== +volumes: + postgres_data_dev: + name: amici-scommesse-postgres-data-dev + mongodb_data_dev: + name: amici-scommesse-mongodb-data-dev + mongodb_config_dev: + name: amici-scommesse-mongodb-config-dev + pgadmin_data_dev: + name: amici-scommesse-pgadmin-data-dev diff --git a/backend-node/docker-compose.yml b/backend-node/docker-compose.yml new file mode 100644 index 0000000..ad82493 --- /dev/null +++ b/backend-node/docker-compose.yml @@ -0,0 +1,123 @@ +version: '3.8' + +services: + # ======================================== + # PostgreSQL Database + # ======================================== + postgres: + image: postgres:16-alpine + container_name: amici-scommesse-postgres + restart: unless-stopped + environment: + POSTGRES_DB: ${POSTGRES_DB:-amici_scommesse} + POSTGRES_USER: ${POSTGRES_USER:-admin} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-admin} + PGDATA: /var/lib/postgresql/data/pgdata + ports: + - "${POSTGRES_PORT:-5432}:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-admin} -d ${POSTGRES_DB:-amici_scommesse}"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - amici-scommesse-network + + # ======================================== + # MongoDB Database + # ======================================== + mongodb: + image: mongo:7-jammy + container_name: amici-scommesse-mongodb + restart: unless-stopped + environment: + MONGO_INITDB_ROOT_USERNAME: ${MONGO_USER:-root} + MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD:-root@servovio} + MONGO_INITDB_DATABASE: ${MONGO_DB:-amici-scommesse} + ports: + - "${MONGO_PORT:-27017}:27017" + volumes: + - mongodb_data:/data/db + - mongodb_config:/data/configdb + healthcheck: + test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - amici-scommesse-network + + # ======================================== + # Backend Node.js (Production) + # ======================================== + backend: + build: + context: . + dockerfile: Dockerfile + target: production + container_name: amici-scommesse-backend + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + mongodb: + condition: service_healthy + environment: + NODE_ENV: production + PORT: ${PORT:-3000} + + # PostgreSQL connection (using service name as host) + POSTGRES_HOST: postgres + POSTGRES_PORT: 5432 + POSTGRES_DB: ${POSTGRES_DB:-amici_scommesse} + POSTGRES_USER: ${POSTGRES_USER:-admin} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-admin} + POSTGRES_URI: postgres://${POSTGRES_USER:-admin}:${POSTGRES_PASSWORD:-admin}@postgres:5432/${POSTGRES_DB:-amici_scommesse} + + # MongoDB connection (using service name as host) + MONGO_HOST: mongodb + MONGO_PORT: 27017 + MONGO_DB: ${MONGO_DB:-amici-scommesse} + MONGO_USER: ${MONGO_USER:-root} + MONGO_PASSWORD: ${MONGO_PASSWORD:-root@servovio} + MONGO_AUTH_SOURCE: ${MONGO_AUTH_SOURCE:-admin} + MONGO_URI: mongodb://${MONGO_USER:-root}:${MONGO_PASSWORD:-root@servovio}@mongodb:27017/${MONGO_DB:-amici-scommesse}?authSource=${MONGO_AUTH_SOURCE:-admin} + + # Application config + LOG_LEVEL: ${LOG_LEVEL:-info} + ENABLE_SQL_LOGGING: ${ENABLE_SQL_LOGGING:-false} + CORS_ORIGIN: ${CORS_ORIGIN:-*} + MOCK_USER_NAME: ${MOCK_USER_NAME:-Mario Rossi} + DB_SYNC_FORCE: ${DB_SYNC_FORCE:-false} + ports: + - "${PORT:-3000}:3000" + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"] + interval: 30s + timeout: 10s + start_period: 40s + retries: 3 + networks: + - amici-scommesse-network + +# ======================================== +# Networks +# ======================================== +networks: + amici-scommesse-network: + driver: bridge + name: amici-scommesse-network + +# ======================================== +# Volumes +# ======================================== +volumes: + postgres_data: + name: amici-scommesse-postgres-data + mongodb_data: + name: amici-scommesse-mongodb-data + mongodb_config: + name: amici-scommesse-mongodb-config diff --git a/backend-node/init.sql b/backend-node/init.sql new file mode 100644 index 0000000..ca54edc --- /dev/null +++ b/backend-node/init.sql @@ -0,0 +1,77 @@ +-- PostgreSQL schema initialization for core models +-- Derived from Sequelize models: User, Group, Friendship, FriendRequest, and the User<->Group join table + +-- Drop tables in dependency order +DROP TABLE IF EXISTS "UserGroups" CASCADE; +DROP TABLE IF EXISTS friend_requests CASCADE; +DROP TABLE IF EXISTS friendships CASCADE; +DROP TABLE IF EXISTS groups CASCADE; +DROP TABLE IF EXISTS users CASCADE; + +-- users table +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + "email" VARCHAR(255) NOT NULL UNIQUE, + "avatarUrl" VARCHAR(255) NOT NULL, + score INTEGER NOT NULL DEFAULT 0 +); + +-- groups table +CREATE TABLE groups ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL +); + +-- friendships table +-- In-model hook ensures user1Id < user2Id at application level; we also add a DB check for safety. +CREATE TABLE friendships ( + id SERIAL PRIMARY KEY, + "user1Id" INTEGER NOT NULL, + "user2Id" INTEGER NOT NULL, + CONSTRAINT friendships_user_order_chk CHECK ("user1Id" < "user2Id"), + CONSTRAINT friendships_user_pair_uniq UNIQUE ("user1Id", "user2Id"), + CONSTRAINT friendships_user1_fk FOREIGN KEY ("user1Id") REFERENCES users(id) ON DELETE CASCADE, + CONSTRAINT friendships_user2_fk FOREIGN KEY ("user2Id") REFERENCES users(id) ON DELETE CASCADE +); + +-- friend_requests table (with Sequelize default timestamps) +CREATE TABLE friend_requests ( + id SERIAL PRIMARY KEY, + status VARCHAR(50) NOT NULL DEFAULT 'PENDING', + "fromUserId" INTEGER NOT NULL, + "toUserId" INTEGER NOT NULL, + "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), + "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), + CONSTRAINT friend_requests_from_fk FOREIGN KEY ("fromUserId") REFERENCES users(id) ON DELETE CASCADE, + CONSTRAINT friend_requests_to_fk FOREIGN KEY ("toUserId") REFERENCES users(id) ON DELETE CASCADE +); + +-- Optional: keep updatedAt current via trigger (Sequelize usually updates it in app code) +-- You can uncomment the below if you want DB-side auto update +-- CREATE OR REPLACE FUNCTION set_updated_at() +-- RETURNS TRIGGER AS $$ +-- BEGIN +-- NEW."updatedAt" = NOW(); +-- RETURN NEW; +-- END; +-- $$ LANGUAGE plpgsql; +-- CREATE TRIGGER friend_requests_set_updated_at +-- BEFORE UPDATE ON friend_requests +-- FOR EACH ROW +-- EXECUTE FUNCTION set_updated_at(); + +-- User <-> Group many-to-many join table +CREATE TABLE "UserGroups" ( + "userId" INTEGER NOT NULL, + "groupId" INTEGER NOT NULL, + CONSTRAINT usergroups_user_fk FOREIGN KEY ("userId") REFERENCES users(id) ON DELETE CASCADE, + CONSTRAINT usergroups_group_fk FOREIGN KEY ("groupId") REFERENCES groups(id) ON DELETE CASCADE, + CONSTRAINT usergroups_user_group_uniq UNIQUE ("userId", "groupId") +); + +-- Helpful indexes +CREATE INDEX IF NOT EXISTS idx_friend_requests_from ON friend_requests ("fromUserId"); +CREATE INDEX IF NOT EXISTS idx_friend_requests_to ON friend_requests ("toUserId"); +CREATE INDEX IF NOT EXISTS idx_friendships_user1 ON friendships ("user1Id"); +CREATE INDEX IF NOT EXISTS idx_friendships_user2 ON friendships ("user2Id"); diff --git a/backend-node/middleware/auth.mock.js b/backend-node/middleware/auth.mock.js new file mode 100644 index 0000000..a26609e --- /dev/null +++ b/backend-node/middleware/auth.mock.js @@ -0,0 +1,19 @@ +const config = require('../config'); + +module.exports = (sequelize) => { + const { User } = sequelize.models; + let MOCK_USER_ID = null; + + return async (req, res, next) => { + if (!MOCK_USER_ID) { + const user = await User.findOne({ + where: { name: config.MOCK_AUTH.DEFAULT_USER_NAME }, + }); + if (user) { + MOCK_USER_ID = user.id; + } + } + req.userId = MOCK_USER_ID; + next(); + }; +}; diff --git a/backend-node/middleware/errorHandler.js b/backend-node/middleware/errorHandler.js new file mode 100644 index 0000000..61e7cc4 --- /dev/null +++ b/backend-node/middleware/errorHandler.js @@ -0,0 +1,40 @@ +const { AppError } = require('../shared/errors'); +const ResponseHandler = require('../shared/response.handler'); +const config = require('../config'); + +const errorHandler = (err, req, res, next) => { + if (config.isDevelopment) { + console.error('Error caught by middleware:', err); + } + + if (err instanceof AppError) { + return ResponseHandler.error(res, err.message, err.statusCode, err.errors); + } + + if ( + err.name === 'SequelizeValidationError' || + err.name === 'SequelizeUniqueConstraintError' + ) { + const errors = err.errors + ? err.errors.map((e) => ({ field: e.path, message: e.message })) + : null; + return ResponseHandler.badRequest(res, 'Validation error', errors); + } + + if (err.name === 'ValidationError' && err.errors) { + const errors = Object.keys(err.errors).map((key) => ({ + field: key, + message: err.errors[key].message, + })); + return ResponseHandler.badRequest(res, 'Validation error', errors); + } + + if (err.name === 'CastError') { + return ResponseHandler.badRequest(res, 'Invalid ID format'); + } + + const message = config.isDevelopment ? err.message : 'Internal server error'; + return ResponseHandler.internalError(res, message, err); +}; + +module.exports = errorHandler; diff --git a/backend-node/middleware/validation.js b/backend-node/middleware/validation.js new file mode 100644 index 0000000..0a037d6 --- /dev/null +++ b/backend-node/middleware/validation.js @@ -0,0 +1,23 @@ +const { ValidationError } = require('../shared/errors'); + +const validate = (schema) => { + return (req, res, next) => { + const { error, value } = schema.validate(req.body, { + abortEarly: false, + stripUnknown: true, + }); + + if (error) { + const errors = error.details.map((detail) => ({ + field: detail.path.join('.'), + message: detail.message, + })); + return next(new ValidationError('Validation failed', errors)); + } + + req.body = value; + next(); + }; +}; + +module.exports = { validate }; diff --git a/backend-node/models/associations.js b/backend-node/models/associations.js new file mode 100644 index 0000000..a0715d0 --- /dev/null +++ b/backend-node/models/associations.js @@ -0,0 +1,21 @@ +module.exports = (sequelize) => { + const { User, Group, FriendRequest, Friendship } = sequelize.models; + + // User <-> Group: Many-to-Many + // A User can be in many Groups, and a Group can have many Users (members). + const UserGroup = sequelize.define('UserGroup', {}, { timestamps: false }); + User.belongsToMany(Group, { through: UserGroup, foreignKey: 'userId' }); + Group.belongsToMany(User, { through: UserGroup, as: 'members', foreignKey: 'groupId' }); + + // Friendship is a direct link between two users. + // user1Id will always be less than user2Id to ensure uniqueness. + Friendship.belongsTo(User, { as: 'user1', foreignKey: 'user1Id' }); + Friendship.belongsTo(User, { as: 'user2', foreignKey: 'user2Id' }); + + // User <-> FriendRequest: One-to-Many + // A User can send many friend requests and receive many friend requests. + User.hasMany(FriendRequest, { as: 'sentRequests', foreignKey: 'fromUserId', onDelete: 'CASCADE' }); + User.hasMany(FriendRequest, { as: 'receivedRequests', foreignKey: 'toUserId', onDelete: 'CASCADE' }); + FriendRequest.belongsTo(User, { as: 'fromUser', foreignKey: 'fromUserId' }); + FriendRequest.belongsTo(User, { as: 'toUser', foreignKey: 'toUserId' }); +}; diff --git a/backend-node/models/bet.model.js b/backend-node/models/bet.model.js new file mode 100644 index 0000000..0f4cae2 --- /dev/null +++ b/backend-node/models/bet.model.js @@ -0,0 +1,30 @@ +const mongoose = require('mongoose'); +const { Schema } = mongoose; + +const BetParticipantSchema = new Schema({ + user: { type: Number, required: true }, // Stores the PostgreSQL User ID + role: { type: String, enum: ['PARTICIPANT', 'OBSERVER'], required: true }, +}, { _id: false }); + +const BetSchema = new mongoose.Schema({ + title: { type: String, required: true }, + description: { type: String }, + stake: { type: String, required: true }, + participants: [BetParticipantSchema], + status: { type: String, enum: ['ACTIVE', 'AWAITING_VALIDATION', 'COMPLETED', 'EXPIRED'], required: true }, + startDate: { type: Date, default: Date.now }, + endDate: { type: Date, default: null }, + isPublic: { type: Boolean, default: false }, + groupId: { type: String, default: null }, // Stores the PostgreSQL Group ID + winner: { type: Number, default: null }, // Stores the PostgreSQL User ID +}); + +BetSchema.set('toJSON', { + virtuals: true, + versionKey: false, + transform: function (doc, ret) { + delete ret._id; + } +}); + +module.exports = mongoose.model('Bet', BetSchema); \ No newline at end of file diff --git a/backend-node/models/friendRequest.model.js b/backend-node/models/friendRequest.model.js new file mode 100644 index 0000000..e774168 --- /dev/null +++ b/backend-node/models/friendRequest.model.js @@ -0,0 +1,21 @@ +const { DataTypes } = require('sequelize'); + +module.exports = (sequelize) => { + const FriendRequest = sequelize.define('FriendRequest', { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true + }, + status: { + type: DataTypes.STRING, + defaultValue: 'PENDING', + allowNull: false + } + // Foreign keys 'fromUserId' and 'toUserId' are added via associations + }, { + // Uses default `createdAt` and `updatedAt` timestamps from Sequelize + tableName: 'friend_requests' + }); + return FriendRequest; +}; diff --git a/backend-node/models/friendship.model.js b/backend-node/models/friendship.model.js new file mode 100644 index 0000000..1861f51 --- /dev/null +++ b/backend-node/models/friendship.model.js @@ -0,0 +1,25 @@ +const { DataTypes } = require('sequelize'); + +module.exports = (sequelize) => { + const Friendship = sequelize.define('Friendship', { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true + } + // Foreign keys 'user1Id' and 'user2Id' are added via associations + }, { + timestamps: false, + tableName: 'friendships', + hooks: { + // Ensure user1Id is always less than user2Id to guarantee + // a unique friendship pair regardless of creation order. + beforeValidate: (friendship) => { + if (friendship.user1Id > friendship.user2Id) { + [friendship.user1Id, friendship.user2Id] = [friendship.user2Id, friendship.user1Id]; + } + } + } + }); + return Friendship; +}; diff --git a/backend-node/models/group.model.js b/backend-node/models/group.model.js new file mode 100644 index 0000000..92fd610 --- /dev/null +++ b/backend-node/models/group.model.js @@ -0,0 +1,19 @@ +const { DataTypes } = require('sequelize'); + +module.exports = (sequelize) => { + const Group = sequelize.define('Group', { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true + }, + name: { + type: DataTypes.STRING, + allowNull: false + } + }, { + timestamps: false, + tableName: 'groups' + }); + return Group; +}; diff --git a/backend-node/models/user.model.js b/backend-node/models/user.model.js new file mode 100644 index 0000000..4ae6311 --- /dev/null +++ b/backend-node/models/user.model.js @@ -0,0 +1,35 @@ +const { DataTypes } = require('sequelize'); + +module.exports = (sequelize) => { + const User = sequelize.define('User', { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true + }, + name: { + type: DataTypes.STRING, + allowNull: false + }, + email: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + validate: { + isEmail: true + } + }, + avatarUrl: { + type: DataTypes.STRING, + allowNull: false + }, + score: { + type: DataTypes.INTEGER, + defaultValue: 0 + }, + }, { + timestamps: false, + tableName: 'users' + }); + return User; +}; diff --git a/backend-node/openapi.yaml b/backend-node/openapi.yaml new file mode 100644 index 0000000..60fe647 --- /dev/null +++ b/backend-node/openapi.yaml @@ -0,0 +1,1064 @@ +openapi: 3.0.3 +info: + title: Amici Scommesse API + version: 1.1.0 + description: | + API per l'applicazione Amici Scommesse - Sistema di gestione scommesse tra amici. + + **Architettura**: L'API utilizza una architettura a layer con dependency injection, + validazione automatica input, e formato risposte standardizzato. + + **Versioning**: + - `/api/v1/*` - API corrente (formato standardizzato) + - `/api/legacy/*` - API legacy (backward compatibility) + + **Autenticazione**: Mock authentication (Mario Rossi hardcoded). JWT da implementare. + contact: + name: API Support + email: support@amiciscommesse.com + license: + name: ISC + +servers: + - url: http://localhost:3000/api/v1 + description: Development server (v1) + - url: http://localhost:3000/api/legacy + description: Legacy API (backward compatibility) + +tags: + - name: Groups + description: Gestione gruppi di utenti + - name: Bets + description: Gestione scommesse + - name: Friend Requests + description: Gestione richieste di amicizia + - name: Users + description: Gestione profilo utente + - name: Initial Data + description: Caricamento dati iniziali + - name: Health + description: Health check endpoint + +components: + schemas: + # ===== Success Response Wrapper ===== + SuccessResponse: + type: object + required: + - success + - timestamp + properties: + success: + type: boolean + example: true + message: + type: string + example: Operation successful + data: + type: object + timestamp: + type: string + format: date-time + example: '2024-01-01T00:00:00.000Z' + + # ===== Error Response Wrapper ===== + ErrorResponse: + type: object + required: + - success + - message + - timestamp + properties: + success: + type: boolean + example: false + message: + type: string + example: Error message + errors: + type: array + items: + type: object + properties: + field: + type: string + example: name + message: + type: string + example: Name is required + timestamp: + type: string + format: date-time + example: '2024-01-01T00:00:00.000Z' + + # ===== User ===== + User: + type: object + properties: + id: + type: integer + example: 1 + name: + type: string + example: Mario Rossi + email: + type: string + format: email + example: mario.rossi@example.com + avatarUrl: + type: string + nullable: true + example: https://example.com/avatar.jpg + score: + type: integer + example: 100 + + # ===== Group ===== + Group: + type: object + properties: + id: + type: integer + example: 1 + name: + type: string + example: Gruppo Calcio + members: + type: array + items: + $ref: '#/components/schemas/User' + + # ===== Participant ===== + Participant: + type: object + properties: + user: + oneOf: + - type: integer + - $ref: '#/components/schemas/User' + role: + type: string + enum: [PARTICIPANT, OBSERVER] + example: PARTICIPANT + + # ===== Bet ===== + Bet: + type: object + properties: + id: + type: string + example: 507f1f77bcf86cd799439011 + title: + type: string + example: Chi vince il campionato? + description: + type: string + nullable: true + example: Scommessa sul vincitore del campionato 2024 + stake: + type: string + example: 50 euro + participants: + type: array + items: + $ref: '#/components/schemas/Participant' + status: + type: string + enum: [ACTIVE, AWAITING_VALIDATION, COMPLETED, EXPIRED] + example: ACTIVE + startDate: + type: string + format: date-time + example: '2024-01-01T00:00:00.000Z' + endDate: + type: string + format: date-time + nullable: true + example: '2024-12-31T23:59:59.000Z' + isPublic: + type: boolean + example: false + groupId: + type: string + nullable: true + example: '1' + winner: + oneOf: + - type: integer + - $ref: '#/components/schemas/User' + nullable: true + + # ===== Friend Request ===== + FriendRequest: + type: object + properties: + id: + type: integer + example: 1 + fromUserId: + type: integer + example: 1 + toUserId: + type: integer + example: 2 + status: + type: string + enum: [PENDING, ACCEPTED, REJECTED] + example: PENDING + fromUser: + $ref: '#/components/schemas/User' + toUser: + $ref: '#/components/schemas/User' + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + + # ===== Create Group Request ===== + CreateGroupRequest: + type: object + required: + - name + - members + properties: + name: + type: string + minLength: 1 + maxLength: 100 + example: Gruppo Calcio + members: + type: array + minItems: 1 + items: + type: integer + example: [1, 2, 3] + + # ===== Update Group Request ===== + UpdateGroupRequest: + type: object + properties: + name: + type: string + minLength: 1 + maxLength: 100 + example: Nuovo Nome Gruppo + + # ===== Add Members Request ===== + AddMembersRequest: + type: object + required: + - userIds + properties: + userIds: + type: array + minItems: 1 + items: + type: integer + example: [4, 5] + + # ===== Create Bet Request ===== + CreateBetRequest: + type: object + required: + - title + - stake + - participants + properties: + title: + type: string + minLength: 1 + maxLength: 200 + example: Chi vince il campionato? + description: + type: string + maxLength: 1000 + example: Scommessa sul vincitore + stake: + type: string + minLength: 1 + example: 50 euro + participants: + type: array + minItems: 1 + items: + type: object + required: + - user + - role + properties: + user: + type: integer + role: + type: string + enum: [PARTICIPANT, OBSERVER] + example: + - user: 1 + role: PARTICIPANT + - user: 2 + role: PARTICIPANT + isPublic: + type: boolean + default: false + example: false + groupId: + type: string + nullable: true + example: '1' + + # ===== Update Bet Request ===== + UpdateBetRequest: + type: object + properties: + title: + type: string + minLength: 1 + maxLength: 200 + description: + type: string + maxLength: 1000 + stake: + type: string + minLength: 1 + status: + type: string + enum: [ACTIVE, AWAITING_VALIDATION, COMPLETED, EXPIRED] + endDate: + type: string + format: date-time + winner: + type: integer + nullable: true + + # ===== Send Friend Request ===== + SendFriendRequestRequest: + type: object + required: + - identifier + properties: + identifier: + oneOf: + - type: integer + - type: string + example: 2 + description: User ID, name, or email + + # ===== Update User Request ===== + UpdateUserRequest: + type: object + properties: + name: + type: string + minLength: 1 + maxLength: 100 + example: Mario Rossi + email: + type: string + format: email + example: mario.rossi@example.com + avatarUrl: + type: string + format: uri + nullable: true + example: https://example.com/avatar.jpg + + # ===== Initial Data ===== + InitialData: + type: object + properties: + currentUser: + $ref: '#/components/schemas/User' + users: + type: array + items: + $ref: '#/components/schemas/User' + friends: + type: array + items: + $ref: '#/components/schemas/User' + friendRequests: + type: array + items: + $ref: '#/components/schemas/FriendRequest' + groups: + type: array + items: + $ref: '#/components/schemas/Group' + userBets: + type: array + items: + $ref: '#/components/schemas/Bet' + publicBets: + type: array + items: + $ref: '#/components/schemas/Bet' + + # ===== Health Status ===== + HealthStatus: + type: object + properties: + status: + type: string + enum: [healthy, unhealthy] + example: healthy + timestamp: + type: string + format: date-time + services: + type: object + properties: + postgres: + type: string + example: connected + mongodb: + type: string + example: connected + + responses: + ValidationError: + description: Validation error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + success: false + message: Validation failed + errors: + - field: name + message: Name is required + timestamp: '2024-01-01T00:00:00.000Z' + + NotFound: + description: Resource not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + success: false + message: Resource not found + timestamp: '2024-01-01T00:00:00.000Z' + + InternalError: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + success: false + message: Internal server error + timestamp: '2024-01-01T00:00:00.000Z' + +paths: + # ==================== GROUPS ==================== + /groups: + get: + tags: + - Groups + summary: Lista tutti i gruppi + description: Recupera la lista di tutti i gruppi con i loro membri + responses: + '200': + description: Lista gruppi recuperata con successo + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/SuccessResponse' + - type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Group' + '500': + $ref: '#/components/responses/InternalError' + + post: + tags: + - Groups + summary: Crea un nuovo gruppo + description: Crea un nuovo gruppo con i membri specificati + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateGroupRequest' + responses: + '201': + description: Gruppo creato con successo + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/SuccessResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/Group' + '400': + $ref: '#/components/responses/ValidationError' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalError' + + /groups/{id}: + get: + tags: + - Groups + summary: Dettaglio gruppo + description: Recupera i dettagli di un gruppo specifico con i membri + parameters: + - name: id + in: path + required: true + schema: + type: integer + example: 1 + responses: + '200': + description: Gruppo recuperato con successo + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/SuccessResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/Group' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalError' + + put: + tags: + - Groups + summary: Aggiorna gruppo + description: Aggiorna le informazioni di un gruppo + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateGroupRequest' + responses: + '200': + description: Gruppo aggiornato con successo + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/SuccessResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/Group' + '400': + $ref: '#/components/responses/ValidationError' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalError' + + delete: + tags: + - Groups + summary: Elimina gruppo + description: Elimina un gruppo esistente + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Gruppo eliminato con successo + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/SuccessResponse' + - type: object + properties: + data: + type: 'null' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalError' + + /groups/{id}/members: + put: + tags: + - Groups + summary: Aggiungi membri al gruppo + description: Aggiunge uno o più membri a un gruppo esistente + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AddMembersRequest' + responses: + '200': + description: Membri aggiunti con successo + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/SuccessResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/Group' + '400': + $ref: '#/components/responses/ValidationError' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalError' + + /groups/{id}/members/{userId}: + delete: + tags: + - Groups + summary: Rimuovi membro dal gruppo + description: Rimuove un membro da un gruppo + parameters: + - name: id + in: path + required: true + schema: + type: integer + - name: userId + in: path + required: true + schema: + type: integer + responses: + '200': + description: Membro rimosso con successo + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/SuccessResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/Group' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalError' + + # ==================== BETS ==================== + /bets: + get: + tags: + - Bets + summary: Lista tutte le scommesse + description: Recupera la lista di tutte le scommesse con dati utente popolati + responses: + '200': + description: Lista scommesse recuperata con successo + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/SuccessResponse' + - type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Bet' + '500': + $ref: '#/components/responses/InternalError' + + post: + tags: + - Bets + summary: Crea una nuova scommessa + description: Crea una nuova scommessa con partecipanti + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateBetRequest' + responses: + '201': + description: Scommessa creata con successo + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/SuccessResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/Bet' + '400': + $ref: '#/components/responses/ValidationError' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalError' + + /bets/{id}: + get: + tags: + - Bets + summary: Dettaglio scommessa + description: Recupera i dettagli di una scommessa specifica + parameters: + - name: id + in: path + required: true + schema: + type: string + example: 507f1f77bcf86cd799439011 + responses: + '200': + description: Scommessa recuperata con successo + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/SuccessResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/Bet' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalError' + + put: + tags: + - Bets + summary: Aggiorna scommessa + description: Aggiorna i dettagli di una scommessa + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateBetRequest' + responses: + '200': + description: Scommessa aggiornata con successo + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/SuccessResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/Bet' + '400': + $ref: '#/components/responses/ValidationError' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalError' + + delete: + tags: + - Bets + summary: Elimina scommessa + description: Elimina una scommessa esistente + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + '200': + description: Scommessa eliminata con successo + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/SuccessResponse' + - type: object + properties: + data: + type: 'null' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalError' + + # ==================== FRIEND REQUESTS ==================== + /friend-requests: + get: + tags: + - Friend Requests + summary: Lista amici + description: Recupera la lista di amici dell'utente corrente + responses: + '200': + description: Lista amici recuperata con successo + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/SuccessResponse' + - type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/User' + '500': + $ref: '#/components/responses/InternalError' + + post: + tags: + - Friend Requests + summary: Invia richiesta di amicizia + description: Invia una richiesta di amicizia a un utente + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SendFriendRequestRequest' + responses: + '201': + description: Richiesta inviata con successo + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/SuccessResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/FriendRequest' + '400': + $ref: '#/components/responses/ValidationError' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: Conflict - Already friends or request exists + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + $ref: '#/components/responses/InternalError' + + /friend-requests/pending: + get: + tags: + - Friend Requests + summary: Lista richieste pending + description: Recupera le richieste di amicizia pending per l'utente corrente + responses: + '200': + description: Lista richieste recuperata con successo + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/SuccessResponse' + - type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/FriendRequest' + '500': + $ref: '#/components/responses/InternalError' + + /friend-requests/{id}/accept: + post: + tags: + - Friend Requests + summary: Accetta richiesta di amicizia + description: Accetta una richiesta di amicizia pending + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Richiesta accettata con successo + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/SuccessResponse' + - type: object + properties: + data: + type: object + properties: + message: + type: string + example: Friend request accepted successfully + '400': + $ref: '#/components/responses/ValidationError' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalError' + + /friend-requests/{id}: + delete: + tags: + - Friend Requests + summary: Rifiuta/cancella richiesta + description: Rifiuta o cancella una richiesta di amicizia + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Richiesta rifiutata con successo + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/SuccessResponse' + - type: object + properties: + data: + type: object + properties: + message: + type: string + example: Friend request rejected successfully + '400': + $ref: '#/components/responses/ValidationError' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalError' + + # ==================== USERS ==================== + /users/current: + put: + tags: + - Users + summary: Aggiorna profilo utente + description: Aggiorna il profilo dell'utente corrente + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateUserRequest' + responses: + '200': + description: Profilo aggiornato con successo + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/SuccessResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/User' + '400': + $ref: '#/components/responses/ValidationError' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalError' + + # ==================== INITIAL DATA ==================== + /initial-data: + get: + tags: + - Initial Data + summary: Carica dati iniziali + description: | + Carica tutti i dati iniziali necessari per l'applicazione in una singola chiamata. + Include: utente corrente, lista utenti, amici, richieste di amicizia, gruppi, scommesse. + responses: + '200': + description: Dati iniziali caricati con successo + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/SuccessResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/InitialData' + '500': + $ref: '#/components/responses/InternalError' + +# Health Check (non versioned) +/health: + get: + tags: + - Health + summary: Health check + description: Verifica lo stato dei database (PostgreSQL e MongoDB) + servers: + - url: http://localhost:3000 + description: Health check endpoint (non versioned) + responses: + '200': + description: Sistema healthy + content: + application/json: + schema: + $ref: '#/components/schemas/HealthStatus' + '503': + description: Sistema unhealthy + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: unhealthy + timestamp: + type: string + format: date-time + error: + type: string + example: Database connection failed diff --git a/backend-node/package-lock.json b/backend-node/package-lock.json new file mode 100644 index 0000000..d755889 --- /dev/null +++ b/backend-node/package-lock.json @@ -0,0 +1,4780 @@ +{ + "name": "amici-scommesse-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "amici-scommesse-backend", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "awilix": "^10.0.2", + "cors": "^2.8.5", + "express": "^4.19.2", + "joi": "^17.12.1", + "mongoose": "^8.4.0", + "pg": "^8.11.5", + "pg-hstore": "^2.3.4", + "sequelize": "^6.37.3" + }, + "devDependencies": { + "eslint": "^8.56.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-prettier": "^5.1.3", + "prettier": "^3.2.4" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/eslintrc/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "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/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.1.tgz", + "integrity": "sha512-6nZrq5kfAz0POWyhljnbWQQJQ5uT8oE2ddX303q1uY0tWsivWKgBDXBBvuFPwOqRRalXJuVO9EjOdVtuhLX0zg==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "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/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "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==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.7.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.2.tgz", + "integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.14.0" + } + }, + "node_modules/@types/validator": { + "version": "13.15.3", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.3.tgz", + "integrity": "sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q==", + "license": "MIT" + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "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/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-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/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/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/awilix": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/awilix/-/awilix-10.0.2.tgz", + "integrity": "sha512-hFatb7eZFdtiWjjmGRSm/K/uxZpmcBlM+YoeMB3VpOPXk3xa6+7zctg3LRbUzoimom5bwGrePF0jXReO6b4zNQ==", + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "fast-glob": "^3.3.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "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==", + "dev": true, + "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/bson": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "license": "MIT", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "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/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/confusing-browser-globals": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", + "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dottie": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", + "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==", + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "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/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-airbnb-base": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", + "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", + "dev": true, + "license": "MIT", + "dependencies": { + "confusing-browser-globals": "^1.0.10", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5", + "semver": "^6.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "peerDependencies": { + "eslint": "^7.32.0 || ^8.2.0", + "eslint-plugin-import": "^2.25.2" + } + }, + "node_modules/eslint-config-airbnb-base/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz", + "integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.12" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/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/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-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "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/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "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": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "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/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflection": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", + "engines": [ + "node >= 0.4.0" + ], + "license": "MIT" + }, + "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": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "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/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" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "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/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/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.48", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", + "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mongodb": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", + "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.3.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.19.1.tgz", + "integrity": "sha512-oB7hGQJn4f8aebqE7mhE54EReb5cxVgpCxQCQj0K/cK3q4J3Tg08nFP6sM52nJ4Hlm8jsDnhVYpqIITZUAhckQ==", + "license": "MIT", + "dependencies": { + "bson": "^6.10.4", + "kareem": "2.6.3", + "mongodb": "~6.20.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mongoose/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/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "license": "MIT", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/mquery/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mquery/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/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "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": "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/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "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/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "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-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.3", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.7" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", + "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "license": "MIT" + }, + "node_modules/pg-hstore": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/pg-hstore/-/pg-hstore-2.3.4.tgz", + "integrity": "sha512-N3SGs/Rf+xA1M2/n0JBiXFDVMzdekwLZLAO0g7mpDY9ouX+fDI7jS6kTq3JujmYbtNSJ53TJ0q4G98KVZSM4EA==", + "license": "MIT", + "dependencies": { + "underscore": "^1.13.1" + }, + "engines": { + "node": ">= 0.8.x" + } + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "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/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/retry-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz", + "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==", + "license": "MIT" + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/sequelize": { + "version": "6.37.7", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.7.tgz", + "integrity": "sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/sequelize" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.8", + "@types/validator": "^13.7.17", + "debug": "^4.3.4", + "dottie": "^2.0.6", + "inflection": "^1.13.4", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", + "pg-connection-string": "^2.6.1", + "retry-as-promised": "^7.0.4", + "semver": "^7.5.4", + "sequelize-pool": "^7.1.0", + "toposort-class": "^1.0.1", + "uuid": "^8.3.2", + "validator": "^13.9.0", + "wkx": "^0.5.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependenciesMeta": { + "ibm_db": { + "optional": true + }, + "mariadb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-hstore": { + "optional": true + }, + "snowflake-sdk": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/sequelize-pool": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", + "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/sequelize/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/sequelize/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/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/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/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT" + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/synckit": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "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/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/toposort-class": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==", + "license": "MIT" + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", + "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", + "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/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validator": { + "version": "13.15.15", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", + "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "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/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wkx": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", + "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/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/backend-node/package.json b/backend-node/package.json new file mode 100644 index 0000000..989d71a --- /dev/null +++ b/backend-node/package.json @@ -0,0 +1,46 @@ +{ + "name": "amici-scommesse-backend", + "version": "1.0.0", + "description": "Backend for the Amici Scommesse app", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "node server.js", + "test": "echo \"Error: no test specified\" && exit 1", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "format": "prettier --write \"**/*.js\"", + "docker:dev": "docker compose -f docker-compose.dev.yml up -d", + "docker:dev:logs": "docker compose -f docker-compose.dev.yml logs -f backend", + "docker:dev:down": "docker compose -f docker-compose.dev.yml down", + "docker:dev:rebuild": "docker compose -f docker-compose.dev.yml up -d --build", + "docker:prod": "docker compose up -d", + "docker:prod:logs": "docker compose logs -f backend", + "docker:prod:down": "docker compose down", + "docker:prod:rebuild": "docker compose up -d --build", + "docker:clean": "docker compose down -v && docker system prune -f", + "docker:ps": "docker compose ps", + "docker:restart": "docker compose restart backend" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "awilix": "^10.0.2", + "cors": "^2.8.5", + "express": "^4.19.2", + "joi": "^17.12.1", + "mongoose": "^8.4.0", + "pg": "^8.11.5", + "pg-hstore": "^2.3.4", + "sequelize": "^6.37.3" + }, + "devDependencies": { + "eslint": "^8.56.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-prettier": "^5.1.3", + "prettier": "^3.2.4" + } +} \ No newline at end of file diff --git a/backend-node/repositories/mongo/bets.repository.js b/backend-node/repositories/mongo/bets.repository.js new file mode 100644 index 0000000..36d4176 --- /dev/null +++ b/backend-node/repositories/mongo/bets.repository.js @@ -0,0 +1,102 @@ +const Bet = require('../../models/bet.model'); +const { NotFoundError, DatabaseError } = require('../../shared/errors'); +const config = require('../../config'); + +class BetsRepository { + async findAll(filter = {}, options = {}) { + try { + const { sort = { startDate: -1 }, limit, skip } = options; + let query = Bet.find(filter); + + if (sort) query = query.sort(sort); + if (limit) query = query.limit(limit); + if (skip) query = query.skip(skip); + + return await query.exec(); + } catch (error) { + throw new DatabaseError('Failed to fetch bets', error); + } + } + + async findById(id) { + try { + const bet = await Bet.findById(id); + if (!bet) { + throw new NotFoundError('Bet not found'); + } + return bet; + } catch (error) { + if (error instanceof NotFoundError) throw error; + if (error.name === 'CastError') { + throw new NotFoundError('Bet not found'); + } + throw new DatabaseError(`Failed to fetch bet with id ${id}`, error); + } + } + + async findByUser(userId) { + try { + return await Bet.find({ 'participants.user': userId }).sort({ startDate: -1 }); + } catch (error) { + throw new DatabaseError(`Failed to fetch bets for user ${userId}`, error); + } + } + + async findByGroup(groupId) { + try { + return await Bet.find({ groupId }).sort({ startDate: -1 }); + } catch (error) { + throw new DatabaseError(`Failed to fetch bets for group ${groupId}`, error); + } + } + + async findPublicBetsExcludingUser(userId) { + try { + return await Bet.find({ + isPublic: true, + 'participants.user': { $ne: userId }, + }).sort({ startDate: -1 }); + } catch (error) { + throw new DatabaseError('Failed to fetch public bets', error); + } + } + + async create(betData) { + try { + const bet = new Bet({ + ...betData, + startDate: new Date(), + status: config.BET_STATUS.ACTIVE, + }); + return await bet.save(); + } catch (error) { + throw new DatabaseError('Failed to create bet', error); + } + } + + async update(id, updateData) { + try { + const bet = await this.findById(id); + Object.assign(bet, updateData); + return await bet.save(); + } catch (error) { + if (error instanceof NotFoundError) throw error; + throw new DatabaseError(`Failed to update bet with id ${id}`, error); + } + } + + async delete(id) { + try { + const result = await Bet.findByIdAndDelete(id); + if (!result) { + throw new NotFoundError('Bet not found'); + } + return true; + } catch (error) { + if (error instanceof NotFoundError) throw error; + throw new DatabaseError(`Failed to delete bet with id ${id}`, error); + } + } +} + +module.exports = BetsRepository; diff --git a/backend-node/repositories/postgres/friendRequests.repository.js b/backend-node/repositories/postgres/friendRequests.repository.js new file mode 100644 index 0000000..dd40c91 --- /dev/null +++ b/backend-node/repositories/postgres/friendRequests.repository.js @@ -0,0 +1,99 @@ +const { NotFoundError, DatabaseError } = require('../../shared/errors'); +const { Op } = require('sequelize'); + +class FriendRequestsRepository { + constructor({ sequelize }) { + this.sequelize = sequelize; + this.FriendRequest = sequelize.models.FriendRequest; + this.User = sequelize.models.User; + } + + async findById(id) { + try { + const request = await this.FriendRequest.findByPk(id, { + include: [ + { model: this.User, as: 'fromUser' }, + { model: this.User, as: 'toUser' }, + ], + }); + if (!request) { + throw new NotFoundError('Friend request not found'); + } + return request; + } catch (error) { + if (error instanceof NotFoundError) throw error; + throw new DatabaseError(`Failed to fetch friend request with id ${id}`, error); + } + } + + async findPendingByUserId(userId) { + try { + return await this.FriendRequest.findAll({ + where: { + toUserId: userId, + status: 'PENDING', + }, + include: [ + { model: this.User, as: 'fromUser' }, + { model: this.User, as: 'toUser' }, + ], + }); + } catch (error) { + throw new DatabaseError( + `Failed to fetch pending requests for user ${userId}`, + error + ); + } + } + + async findExisting(fromUserId, toUserId) { + try { + return await this.FriendRequest.findOne({ + where: { + [Op.or]: [ + { fromUserId, toUserId }, + { fromUserId: toUserId, toUserId: fromUserId }, + ], + status: 'PENDING', + }, + }); + } catch (error) { + throw new DatabaseError('Failed to check existing friend request', error); + } + } + + async create(fromUserId, toUserId) { + try { + return await this.FriendRequest.create({ + fromUserId, + toUserId, + status: 'PENDING', + }); + } catch (error) { + throw new DatabaseError('Failed to create friend request', error); + } + } + + async updateStatus(id, status) { + try { + const request = await this.findById(id); + return await request.update({ status }); + } catch (error) { + if (error instanceof NotFoundError) throw error; + throw new DatabaseError('Failed to update friend request status', error); + } + } + + async delete(id) { + try { + const request = await this.findById(id); + await request.destroy(); + return true; + } catch (error) { + if (error instanceof NotFoundError) throw error; + throw new DatabaseError('Failed to delete friend request', error); + } + } +} + +module.exports = FriendRequestsRepository; diff --git a/backend-node/repositories/postgres/friendships.repository.js b/backend-node/repositories/postgres/friendships.repository.js new file mode 100644 index 0000000..fb45e17 --- /dev/null +++ b/backend-node/repositories/postgres/friendships.repository.js @@ -0,0 +1,78 @@ +const { NotFoundError, DatabaseError } = require('../../shared/errors'); +const { Op } = require('sequelize'); + +class FriendshipsRepository { + constructor({ sequelize }) { + this.sequelize = sequelize; + this.Friendship = sequelize.models.Friendship; + this.User = sequelize.models.User; + } + + async findByUserId(userId) { + try { + const friendships = await this.Friendship.findAll({ + where: { + [Op.or]: [{ user1Id: userId }, { user2Id: userId }], + }, + include: [ + { model: this.User, as: 'user1' }, + { model: this.User, as: 'user2' }, + ], + }); + + return friendships.map((friendship) => { + const friend = + friendship.user1.id === userId ? friendship.user2 : friendship.user1; + return friend; + }); + } catch (error) { + throw new DatabaseError(`Failed to fetch friendships for user ${userId}`, error); + } + } + + async exists(user1Id, user2Id) { + try { + const minId = Math.min(user1Id, user2Id); + const maxId = Math.max(user1Id, user2Id); + + const friendship = await this.Friendship.findOne({ + where: { user1Id: minId, user2Id: maxId }, + }); + + return !!friendship; + } catch (error) { + throw new DatabaseError('Failed to check friendship existence', error); + } + } + + async create(user1Id, user2Id) { + try { + const minId = Math.min(user1Id, user2Id); + const maxId = Math.max(user1Id, user2Id); + + return await this.Friendship.create({ + user1Id: minId, + user2Id: maxId, + }); + } catch (error) { + throw new DatabaseError('Failed to create friendship', error); + } + } + + async delete(user1Id, user2Id) { + try { + const minId = Math.min(user1Id, user2Id); + const maxId = Math.max(user1Id, user2Id); + + const result = await this.Friendship.destroy({ + where: { user1Id: minId, user2Id: maxId }, + }); + + return result > 0; + } catch (error) { + throw new DatabaseError('Failed to delete friendship', error); + } + } +} + +module.exports = FriendshipsRepository; diff --git a/backend-node/repositories/postgres/groups.repository.js b/backend-node/repositories/postgres/groups.repository.js new file mode 100644 index 0000000..992ecdd --- /dev/null +++ b/backend-node/repositories/postgres/groups.repository.js @@ -0,0 +1,93 @@ +const { NotFoundError, DatabaseError } = require('../../shared/errors'); + +class GroupsRepository { + constructor({ sequelize }) { + this.sequelize = sequelize; + this.Group = sequelize.models.Group; + this.User = sequelize.models.User; + } + + async findAll(options = {}) { + try { + return await this.Group.findAll(options); + } catch (error) { + throw new DatabaseError('Failed to fetch groups', error); + } + } + + async findById(id, includeMembers = false) { + try { + const options = {}; + if (includeMembers) { + options.include = [ + { + model: this.User, + as: 'members', + through: { attributes: [] }, + }, + ]; + } + const group = await this.Group.findByPk(id, options); + if (!group) { + throw new NotFoundError('Group not found'); + } + return group; + } catch (error) { + if (error instanceof NotFoundError) throw error; + throw new DatabaseError(`Failed to fetch group with id ${id}`, error); + } + } + + async create(groupData) { + try { + return await this.Group.create(groupData); + } catch (error) { + throw new DatabaseError('Failed to create group', error); + } + } + + async addMembers(groupId, userIds) { + try { + const group = await this.findById(groupId); + await group.addMembers(userIds); + return this.findById(groupId, true); + } catch (error) { + if (error instanceof NotFoundError) throw error; + throw new DatabaseError('Failed to add members to group', error); + } + } + + async removeMember(groupId, userId) { + try { + const group = await this.findById(groupId); + await group.removeMember(userId); + return this.findById(groupId, true); + } catch (error) { + if (error instanceof NotFoundError) throw error; + throw new DatabaseError('Failed to remove member from group', error); + } + } + + async update(id, updateData) { + try { + const group = await this.findById(id); + return await group.update(updateData); + } catch (error) { + if (error instanceof NotFoundError) throw error; + throw new DatabaseError(`Failed to update group with id ${id}`, error); + } + } + + async delete(id) { + try { + const group = await this.findById(id); + await group.destroy(); + return true; + } catch (error) { + if (error instanceof NotFoundError) throw error; + throw new DatabaseError(`Failed to delete group with id ${id}`, error); + } + } +} + +module.exports = GroupsRepository; diff --git a/backend-node/repositories/postgres/users.repository.js b/backend-node/repositories/postgres/users.repository.js new file mode 100644 index 0000000..a7ee3a2 --- /dev/null +++ b/backend-node/repositories/postgres/users.repository.js @@ -0,0 +1,68 @@ +const { NotFoundError, DatabaseError } = require('../../shared/errors'); + +class UsersRepository { + constructor({ sequelize }) { + this.sequelize = sequelize; + this.User = sequelize.models.User; + } + + async findAll(options = {}) { + try { + return await this.User.findAll(options); + } catch (error) { + throw new DatabaseError('Failed to fetch users', error); + } + } + + async findById(id, options = {}) { + try { + const user = await this.User.findByPk(id, options); + if (!user) { + throw new NotFoundError('User not found'); + } + return user; + } catch (error) { + if (error instanceof NotFoundError) throw error; + throw new DatabaseError(`Failed to fetch user with id ${id}`, error); + } + } + + async findOne(where, options = {}) { + try { + return await this.User.findOne({ where, ...options }); + } catch (error) { + throw new DatabaseError('Failed to find user', error); + } + } + + async create(userData) { + try { + return await this.User.create(userData); + } catch (error) { + throw new DatabaseError('Failed to create user', error); + } + } + + async update(id, updateData) { + try { + const user = await this.findById(id); + return await user.update(updateData); + } catch (error) { + if (error instanceof NotFoundError) throw error; + throw new DatabaseError(`Failed to update user with id ${id}`, error); + } + } + + async delete(id) { + try { + const user = await this.findById(id); + await user.destroy(); + return true; + } catch (error) { + if (error instanceof NotFoundError) throw error; + throw new DatabaseError(`Failed to delete user with id ${id}`, error); + } + } +} + +module.exports = UsersRepository; diff --git a/backend-node/routes/api.js b/backend-node/routes/api.js new file mode 100644 index 0000000..56b58fb --- /dev/null +++ b/backend-node/routes/api.js @@ -0,0 +1,31 @@ +const express = require('express'); + +// Il router è ora una factory function che riceve l'istanza di sequelize +module.exports = (sequelize) => { + const router = express.Router(); + + // --- Middleware per simulare un utente loggato --- + const { User } = sequelize.models; + const MOCK_USER_NAME = 'Mario Rossi'; + let MOCK_USER_ID = null; + + router.use(async (req, res, next) => { + if (!MOCK_USER_ID) { + const user = await User.findOne({ where: { name: MOCK_USER_NAME } }); + if (user) { + MOCK_USER_ID = user.id; + } + } + req.userId = MOCK_USER_ID; + next(); + }); + + // --- Monta i router da file separati --- + router.use(require('./initial-data.routes')(sequelize)); + router.use('/bets', require('./bets.routes')(sequelize)); + router.use('/groups', require('./groups.routes')(sequelize)); + router.use('/friend-requests', require('./friends.routes')(sequelize)); + router.use('/users', require('./users.routes')(sequelize)); + + return router; +}; diff --git a/backend-node/routes/bets.routes.js b/backend-node/routes/bets.routes.js new file mode 100644 index 0000000..b9d3af9 --- /dev/null +++ b/backend-node/routes/bets.routes.js @@ -0,0 +1,39 @@ +const express = require('express'); +const router = express.Router(); + +// Modello Mongoose per le Scommesse +const Bet = require('../models/bet.model'); + +// Il router è ora una factory function che riceve l'istanza di sequelize +module.exports = (sequelize) => { + const { populateBetsWithUsers } = require('../utils/populateBets')(sequelize); + + // POST /api/bets + router.post('/', async (req, res) => { + try { + const betData = req.body; + // Assicura che gli ID utente dal frontend siano correttamente parsati come interi per lo schema MongoDB + const participants = betData.participants.map(p => ({ + ...p, + user: parseInt(p.user, 10) + })); + + const bet = new Bet({ + ...betData, + participants, + startDate: new Date(), + status: 'ACTIVE' + }); + await bet.save(); + + // Popola nuovamente la scommessa creata per restituire oggetti utente completi + const populatedMongoBet = await Bet.findById(bet._id); + const finalBet = (await populateBetsWithUsers([populatedMongoBet]))[0]; + res.status(201).json(finalBet); + } catch (error) { + res.status(400).json({ message: 'Error creating bet', error: error.message }); + } + }); + + return router; +}; diff --git a/backend-node/routes/friends.routes.js b/backend-node/routes/friends.routes.js new file mode 100644 index 0000000..5d645b6 --- /dev/null +++ b/backend-node/routes/friends.routes.js @@ -0,0 +1,73 @@ +const express = require('express'); +const router = express.Router(); +const { Op } = require('sequelize'); + +module.exports = (sequelize) => { + const { User, Friendship, FriendRequest } = sequelize.models; + + // POST /api/friend-requests + router.post('/', async (req, res) => { + try { + const { identifier } = req.body; + const toUser = await User.findOne({ where: { [Op.or]: [{ email: identifier }, { name: identifier }] } }); + + if (!toUser) return res.status(404).json({ message: 'Utente non trovato.' }); + if (toUser.id === req.userId) return res.status(400).json({ message: 'Non puoi aggiungere te stesso.'}); + + const user1Id = Math.min(req.userId, toUser.id); + const user2Id = Math.max(req.userId, toUser.id); + const friendshipExists = await Friendship.findOne({ where: { user1Id, user2Id } }); + if (friendshipExists) return res.status(400).json({ message: 'Siete già amici.'}); + + const requestExists = await FriendRequest.findOne({ + where: { [Op.or]: [{ fromUserId: req.userId, toUserId: toUser.id }, { fromUserId: toUser.id, toUserId: req.userId }] } + }); + if (requestExists) return res.status(400).json({ message: 'Richiesta già inviata.'}); + + const friendRequest = await FriendRequest.create({ fromUserId: req.userId, toUserId: toUser.id }); + const populatedReq = await FriendRequest.findByPk(friendRequest.id, { + include: [{ model: User, as: 'fromUser' }, { model: User, as: 'toUser' }] + }); + res.status(201).json(populatedReq); + } catch (error) { + res.status(400).json({ message: 'Error sending friend request', error: error.message }); + } + }); + + // POST /api/friend-requests/:id/accept + router.post('/:id/accept', async (req, res) => { + const t = await sequelize.transaction(); + try { + const request = await FriendRequest.findByPk(req.params.id, { transaction: t }); + if (!request || request.toUserId !== req.userId) { + await t.rollback(); + return res.status(404).json({ message: 'Request not found or not for you.' }); + } + + const user1Id = Math.min(request.fromUserId, request.toUserId); + const user2Id = Math.max(request.fromUserId, request.toUserId); + await Friendship.create({ user1Id, user2Id }, { transaction: t }); + await request.destroy({ transaction: t }); + + await t.commit(); + const newFriend = await User.findByPk(request.fromUserId); + res.status(200).json({ message: 'Friend request accepted', newFriend }); + } catch (error) { + await t.rollback(); + res.status(400).json({ message: 'Error accepting friend request', error: error.message }); + } + }); + + // DELETE /api/friend-requests/:id + router.delete('/:id', async (req, res) => { + try { + const count = await FriendRequest.destroy({ where: { id: req.params.id } }); + if (count === 0) return res.status(404).json({ message: 'Request not found.' }); + res.status(200).json({ message: 'Friend request cancelled/declined.' }); + } catch (error) { + res.status(400).json({ message: 'Error deleting friend request', error: error.message }); + } + }); + + return router; +}; diff --git a/backend-node/routes/groups.routes.js b/backend-node/routes/groups.routes.js new file mode 100644 index 0000000..0e12334 --- /dev/null +++ b/backend-node/routes/groups.routes.js @@ -0,0 +1,54 @@ +const express = require('express'); +const router = express.Router(); + +module.exports = (sequelize) => { + const { User, Group } = sequelize.models; + + // POST /api/groups + router.post('/', async (req, res) => { + try { + const { name, members } = req.body; + const group = await Group.create({ name }); + await group.setMembers(members); + const populatedGroup = await Group.findByPk(group.id, { + include: [{ model: User, as: 'members', through: { attributes: [] } }] + }); + res.status(201).json(populatedGroup); + } catch (error) { + res.status(400).json({ message: 'Error creating group', error: error.message }); + } + }); + + // PUT /api/groups/:id/members + router.put('/:id/members', async (req, res) => { + try { + const { userIds } = req.body; + const group = await Group.findByPk(req.params.id); + if (!group) return res.status(404).json({ message: 'Group not found' }); + await group.addMembers(userIds); + const populatedGroup = await Group.findByPk(group.id, { + include: [{ model: User, as: 'members', through: { attributes: [] } }] + }); + res.json(populatedGroup); + } catch (error) { + res.status(400).json({ message: 'Error adding members to group', error: error.message }); + } + }); + + // DELETE /api/groups/:id/members/:userId + router.delete('/:id/members/:userId', async (req, res) => { + try { + const group = await Group.findByPk(req.params.id); + if (!group) return res.status(404).json({ message: 'Group not found' }); + await group.removeMember(req.params.userId); + const populatedGroup = await Group.findByPk(group.id, { + include: [{ model: User, as: 'members', through: { attributes: [] } }] + }); + res.json(populatedGroup); + } catch (error) { + res.status(400).json({ message: 'Error removing member from group', error: error.message }); + } + }); + + return router; +}; diff --git a/backend-node/routes/health.routes.js b/backend-node/routes/health.routes.js new file mode 100644 index 0000000..130b66f --- /dev/null +++ b/backend-node/routes/health.routes.js @@ -0,0 +1,32 @@ +const express = require('express'); + +module.exports = ({ sequelize, mongoose }) => { + const router = express.Router(); + + router.get('/health', async (req, res) => { + try { + await sequelize.authenticate(); + const pgStatus = 'connected'; + + const mongoStatus = + mongoose.connection.readyState === 1 ? 'connected' : 'disconnected'; + + res.json({ + status: 'healthy', + timestamp: new Date().toISOString(), + services: { + postgres: pgStatus, + mongodb: mongoStatus, + }, + }); + } catch (error) { + res.status(503).json({ + status: 'unhealthy', + timestamp: new Date().toISOString(), + error: error.message, + }); + } + }); + + return router; +}; diff --git a/backend-node/routes/initial-data.routes.js b/backend-node/routes/initial-data.routes.js new file mode 100644 index 0000000..42a5924 --- /dev/null +++ b/backend-node/routes/initial-data.routes.js @@ -0,0 +1,59 @@ +const express = require('express'); +const router = express.Router(); +const { Op } = require('sequelize'); + +// Modello Mongoose per le Scommesse +const Bet = require('../models/bet.model'); + +module.exports = (sequelize) => { + const { User, Friendship, FriendRequest } = sequelize.models; + const { populateBetsWithUsers } = require('../utils/populateBets')(sequelize); + + // GET /api/initial-data + router.get('/initial-data', async (req, res) => { + try { + // --- Query PostgreSQL --- + const currentUser = await User.findByPk(req.userId); + const users = await User.findAll(); + + const friendships = await Friendship.findAll({ + where: { [Op.or]: [{ user1Id: req.userId }, { user2Id: req.userId }] }, + include: [{ model: User, as: 'user1' }, { model: User, as: 'user2' }] + }); + const friends = friendships.map(f => (f.user1Id === req.userId ? f.user2 : f.user1)); + + const receivedFriendRequests = await FriendRequest.findAll({ + where: { toUserId: req.userId, status: 'PENDING' }, + include: [{ model: User, as: 'fromUser' }, { model: User, as: 'toUser' }] + }); + + const sentFriendRequests = await FriendRequest.findAll({ + where: { fromUserId: req.userId, status: 'PENDING' }, + include: [{ model: User, as: 'fromUser' }, { model: User, as: 'toUser' }] + }); + + const userGroups = await currentUser.getGroups({ + include: [{ model: User, as: 'members', through: { attributes: [] } }] + }); + + // --- Query MongoDB --- + const mongoBets = await Bet.find({ 'participants.user': req.userId }).sort({ startDate: -1 }); + const mongoPublicBets = await Bet.find({ isPublic: true, 'participants.user': { $ne: req.userId } }); + + // --- Data Stitching --- + const bets = await populateBetsWithUsers(mongoBets); + const publicBets = await populateBetsWithUsers(mongoPublicBets); + + res.json({ + currentUser, users, friends, receivedFriendRequests, sentFriendRequests, + bets, publicBets, groups: userGroups + }); + + } catch (error) { + console.error('Error fetching initial data:', error); + res.status(500).json({ message: 'Error fetching initial data', error: error.message }); + } + }); + + return router; +}; diff --git a/backend-node/routes/users.routes.js b/backend-node/routes/users.routes.js new file mode 100644 index 0000000..28d17d0 --- /dev/null +++ b/backend-node/routes/users.routes.js @@ -0,0 +1,25 @@ +const express = require('express'); +const router = express.Router(); + +module.exports = (sequelize) => { + const { User } = sequelize.models; + + // PUT /api/users/current + router.put('/current', async (req, res) => { + try { + const { name, email } = req.body; + const user = await User.findByPk(req.userId); + if (!user) return res.status(404).json({ message: 'User not found' }); + + user.name = name; + user.email = email; + await user.save(); + + res.json(user); + } catch (error) { + res.status(400).json({ message: 'Error updating user', error: error.message }); + } + }); + + return router; +}; diff --git a/backend-node/routes/v1/bets.routes.js b/backend-node/routes/v1/bets.routes.js new file mode 100644 index 0000000..2487afb --- /dev/null +++ b/backend-node/routes/v1/bets.routes.js @@ -0,0 +1,26 @@ +const express = require('express'); +const { validate } = require('../../middleware/validation'); +const { + createBetSchema, + updateBetSchema, +} = require('../../shared/validators/schemas/bet.schema'); + +module.exports = ({ betsController }) => { + const router = express.Router(); + + const controller = { + getAllBets: (req, res, next) => betsController.getAllBets(req, res, next), + getBetById: (req, res, next) => betsController.getBetById(req, res, next), + createBet: (req, res, next) => betsController.createBet(req, res, next), + updateBet: (req, res, next) => betsController.updateBet(req, res, next), + deleteBet: (req, res, next) => betsController.deleteBet(req, res, next), + }; + + router.get('/', controller.getAllBets); + router.get('/:id', controller.getBetById); + router.post('/', validate(createBetSchema), controller.createBet); + router.put('/:id', validate(updateBetSchema), controller.updateBet); + router.delete('/:id', controller.deleteBet); + + return router; +}; diff --git a/backend-node/routes/v1/friends.routes.js b/backend-node/routes/v1/friends.routes.js new file mode 100644 index 0000000..16e0f81 --- /dev/null +++ b/backend-node/routes/v1/friends.routes.js @@ -0,0 +1,26 @@ +const express = require('express'); +const { validate } = require('../../middleware/validation'); +const { + sendFriendRequestSchema, +} = require('../../shared/validators/schemas/friendRequest.schema'); + +module.exports = ({ friendsController }) => { + const router = express.Router(); + + const controller = { + getFriends: (req, res, next) => friendsController.getFriends(req, res, next), + getPendingRequests: (req, res, next) => + friendsController.getPendingRequests(req, res, next), + sendRequest: (req, res, next) => friendsController.sendRequest(req, res, next), + acceptRequest: (req, res, next) => friendsController.acceptRequest(req, res, next), + rejectRequest: (req, res, next) => friendsController.rejectRequest(req, res, next), + }; + + router.get('/', controller.getFriends); + router.get('/pending', controller.getPendingRequests); + router.post('/', validate(sendFriendRequestSchema), controller.sendRequest); + router.post('/:id/accept', controller.acceptRequest); + router.delete('/:id', controller.rejectRequest); + + return router; +}; diff --git a/backend-node/routes/v1/groups.routes.js b/backend-node/routes/v1/groups.routes.js new file mode 100644 index 0000000..bf5fca6 --- /dev/null +++ b/backend-node/routes/v1/groups.routes.js @@ -0,0 +1,31 @@ +const express = require('express'); +const { validate } = require('../../middleware/validation'); +const { + createGroupSchema, + updateGroupSchema, + addMembersSchema, +} = require('../../shared/validators/schemas/group.schema'); + +module.exports = ({ groupsController }) => { + const router = express.Router(); + + const controller = { + getAllGroups: (req, res, next) => groupsController.getAllGroups(req, res, next), + getGroupById: (req, res, next) => groupsController.getGroupById(req, res, next), + createGroup: (req, res, next) => groupsController.createGroup(req, res, next), + updateGroup: (req, res, next) => groupsController.updateGroup(req, res, next), + deleteGroup: (req, res, next) => groupsController.deleteGroup(req, res, next), + addMembers: (req, res, next) => groupsController.addMembers(req, res, next), + removeMember: (req, res, next) => groupsController.removeMember(req, res, next), + }; + + router.get('/', controller.getAllGroups); + router.get('/:id', controller.getGroupById); + router.post('/', validate(createGroupSchema), controller.createGroup); + router.put('/:id', validate(updateGroupSchema), controller.updateGroup); + router.delete('/:id', controller.deleteGroup); + router.put('/:id/members', validate(addMembersSchema), controller.addMembers); + router.delete('/:id/members/:userId', controller.removeMember); + + return router; +}; diff --git a/backend-node/routes/v1/index.js b/backend-node/routes/v1/index.js new file mode 100644 index 0000000..494739a --- /dev/null +++ b/backend-node/routes/v1/index.js @@ -0,0 +1,21 @@ +const express = require('express'); + +module.exports = (container) => { + const router = express.Router(); + + const controllers = { + groupsController: container.resolve('groupsController'), + betsController: container.resolve('betsController'), + usersController: container.resolve('usersController'), + friendsController: container.resolve('friendsController'), + initialDataController: container.resolve('initialDataController'), + }; + + router.use('/groups', require('./groups.routes')(controllers)); + router.use('/bets', require('./bets.routes')(controllers)); + router.use('/users', require('./users.routes')(controllers)); + router.use('/friend-requests', require('./friends.routes')(controllers)); + router.use('/initial-data', require('./initialData.routes')(controllers)); + + return router; +}; diff --git a/backend-node/routes/v1/initialData.routes.js b/backend-node/routes/v1/initialData.routes.js new file mode 100644 index 0000000..b00a838 --- /dev/null +++ b/backend-node/routes/v1/initialData.routes.js @@ -0,0 +1,13 @@ +const express = require('express'); + +module.exports = ({ initialDataController }) => { + const router = express.Router(); + + const controller = { + getInitialData: (req, res, next) => initialDataController.getInitialData(req, res, next), + }; + + router.get('/', controller.getInitialData); + + return router; +}; diff --git a/backend-node/routes/v1/users.routes.js b/backend-node/routes/v1/users.routes.js new file mode 100644 index 0000000..5e87275 --- /dev/null +++ b/backend-node/routes/v1/users.routes.js @@ -0,0 +1,15 @@ +const express = require('express'); +const { validate } = require('../../middleware/validation'); +const { updateUserSchema } = require('../../shared/validators/schemas/user.schema'); + +module.exports = ({ usersController }) => { + const router = express.Router(); + + const controller = { + updateCurrentUser: (req, res, next) => usersController.updateCurrentUser(req, res, next), + }; + + router.put('/current', validate(updateUserSchema), controller.updateCurrentUser); + + return router; +}; diff --git a/backend-node/seed.js b/backend-node/seed.js new file mode 100644 index 0000000..4734b4d --- /dev/null +++ b/backend-node/seed.js @@ -0,0 +1,71 @@ +const mongoose = require('mongoose'); +const Bet = require('./models/bet.model'); + +const seedDatabase = async (sequelize) => { + const { User, Group, Friendship, FriendRequest } = sequelize.models; + + try { + const userCount = await User.count(); + if (userCount > 0) { + // console.log('PostgreSQL database is not empty. Skipping seed.'); + return; + } + console.log('Seeding databases...'); + + // PostgreSQL tables are already empty due to `sync({ force: true })` in server.js + // Clear MongoDB data + await Bet.deleteMany({}); + + // --- SEED POSTGRESQL --- + + // Create Users + const usersData = [ + { id: 1, name: 'Mario Rossi', email: 'mario.rossi@example.com', avatarUrl: 'https://picsum.photos/id/1005/200/200', score: 120 }, + { id: 2, name: 'Luigi Verdi', email: 'luigi.verdi@example.com', avatarUrl: 'https://picsum.photos/id/1012/200/200', score: 95 }, + { id: 3, name: 'Giovanna Bianchi', email: 'giovanna.bianchi@example.com', avatarUrl: 'https://picsum.photos/id/1027/200/200', score: 150 }, + { id: 4, name: 'Anna Neri', email: 'anna.neri@example.com', avatarUrl: 'https://picsum.photos/id/1011/200/200', score: 80 }, + { id: 5, name: 'Carlo Gialli', email: 'carlo.gialli@example.com', avatarUrl: 'https://picsum.photos/id/20/200/200', score: 200 }, + { id: 6, name: 'Paolo Marroni', email: 'paolo.marroni@example.com', avatarUrl: 'https://picsum.photos/id/30/200/200', score: 50 }, + ]; + await User.bulkCreate(usersData); + + // Create Friendships + const friendshipsData = [ + { user1Id: 1, user2Id: 2 }, + { user1Id: 1, user2Id: 3 }, + { user1Id: 2, user2Id: 3 }, + ]; + await Friendship.bulkCreate(friendshipsData); + + // Create Friend Requests + const friendRequestsData = [ + { fromUserId: 4, toUserId: 1, status: 'PENDING' }, + { fromUserId: 1, toUserId: 5, status: 'PENDING' }, + ]; + await FriendRequest.bulkCreate(friendRequestsData); + + // Create Groups and add members + const group1 = await Group.create({ id: 1, name: 'Amici Stretti' }); + await group1.setMembers([1, 2, 3]); + const group2 = await Group.create({ id: 2, name: 'Colleghi' }); + await group2.setMembers([1, 4, 5]); + + // --- SEED MONGODB --- + + // Create Bets, referencing PostgreSQL IDs + const betsData = [ + { title: 'Chi vince a calcetto?', description: 'Partita di venerdì sera', stake: 'Una pizza', participants: [{ user: 1, role: 'PARTICIPANT' }, { user: 2, role: 'PARTICIPANT' }], status: 'ACTIVE', startDate: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000), endDate: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), isPublic: false, groupId: group1.id.toString() }, + { title: 'Prossimo film al cinema', description: 'Quale film vedremo?', stake: 'Offre i biglietti', participants: [{ user: 1, role: 'PARTICIPANT' }, { user: 3, role: 'PARTICIPANT' }], status: 'AWAITING_VALIDATION', startDate: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000), endDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), isPublic: false, groupId: group1.id.toString() }, + { title: 'Riuscirò a finire il report?', description: 'Entro la deadline di domani!', stake: 'Un caffè', participants: [{ user: 1, role: 'PARTICIPANT' }, { user: 4, role: 'OBSERVER' }], status: 'ACTIVE', startDate: new Date(), endDate: new Date(Date.now() + 1 * 24 * 60 * 60 * 1000), isPublic: true }, + { title: 'Scommessa passata', description: 'Chi ha mangiato più sushi', stake: '€20', participants: [{ user: 1, role: 'PARTICIPANT' }, { user: 2, role: 'PARTICIPANT' }], status: 'COMPLETED', startDate: new Date(Date.now() - 12 * 24 * 60 * 60 * 1000), endDate: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000), isPublic: false, groupId: group1.id.toString(), winner: 2 }, + { title: 'Scommessa pubblica', description: 'Quanti goal segnerà la nazionale?', stake: 'Gloria eterna', participants: [{ user: 5, role: 'PARTICIPANT' }, { user: 6, role: 'PARTICIPANT' }], status: 'ACTIVE', startDate: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000), endDate: null, isPublic: true }, + ]; + await Bet.insertMany(betsData); + + console.log('Database seeded successfully.'); + } catch (error) { + console.error('Error seeding database:', error); + } +}; + +module.exports = { seedDatabase }; \ No newline at end of file diff --git a/backend-node/server.js b/backend-node/server.js new file mode 100644 index 0000000..9d946a1 --- /dev/null +++ b/backend-node/server.js @@ -0,0 +1,64 @@ +const express = require('express'); +const mongoose = require('mongoose'); +const cors = require('cors'); +const config = require('./config'); +const setupContainer = require('./di/container'); +const errorHandler = require('./middleware/errorHandler'); +const mockAuthMiddleware = require('./middleware/auth.mock'); +const { seedDatabase } = require('./seed'); + +const legacyApiRoutes = require('./routes/api'); + +const app = express(); + +app.use(cors()); +app.use(express.json()); + +const container = setupContainer(); + +const sequelize = container.resolve('sequelize'); + +mongoose + .connect(config.database.mongo.uri, config.database.mongo.options) + .then(() => console.log('Connected to MongoDB')) + .catch((err) => { + console.error('MongoDB connection error:', err); + process.exit(1); + }); + +app.use('/api', mockAuthMiddleware(sequelize)); + +app.use('/api/legacy', legacyApiRoutes(sequelize)); + +const v1Routes = require('./routes/v1'); +app.use('/api/v1', v1Routes(container)); +app.use('/api', v1Routes(container)); + +const healthRoutes = require('./routes/health.routes'); +app.use('/', healthRoutes({ sequelize, mongoose })); + +app.use(errorHandler); + +sequelize + .authenticate() + .then(() => { + console.log('Connected to PostgreSQL'); + return sequelize.sync({ force: true }); + }) + .then(() => { + console.log('PostgreSQL models synchronized'); + return seedDatabase(sequelize); + }) + .then(() => { + app.listen(config.port, () => { + console.log(`Server is running on http://localhost:${config.port}`); + console.log(`Environment: ${config.nodeEnv}`); + console.log(`API v1 endpoints available at /api/v1/`); + console.log(`Legacy endpoints available at /api/legacy/`); + console.log(`Health check available at /health`); + }); + }) + .catch((err) => { + console.error('PostgreSQL connection or sync error:', err); + process.exit(1); + }); diff --git a/backend-node/services/bets.service.js b/backend-node/services/bets.service.js new file mode 100644 index 0000000..5026217 --- /dev/null +++ b/backend-node/services/bets.service.js @@ -0,0 +1,103 @@ +const { ValidationError } = require('../shared/errors'); +const config = require('../config'); + +class BetsService { + constructor({ betsRepository, usersRepository, dataStitchingService }) { + this.betsRepository = betsRepository; + this.usersRepository = usersRepository; + this.dataStitchingService = dataStitchingService; + } + + async getAllBets() { + const bets = await this.betsRepository.findAll(); + return await this.dataStitchingService.populateBetsWithUsers(bets); + } + + async getBetById(betId) { + const bet = await this.betsRepository.findById(betId); + const populated = await this.dataStitchingService.populateBetsWithUsers([bet]); + return populated[0]; + } + + async getUserBets(userId) { + const bets = await this.betsRepository.findByUser(userId); + return await this.dataStitchingService.populateBetsWithUsers(bets); + } + + async getGroupBets(groupId) { + const bets = await this.betsRepository.findByGroup(groupId); + return await this.dataStitchingService.populateBetsWithUsers(bets); + } + + async getPublicBetsExcludingUser(userId) { + const bets = await this.betsRepository.findPublicBetsExcludingUser(userId); + return await this.dataStitchingService.populateBetsWithUsers(bets); + } + + async createBet(betData) { + const { title, description, stake, participants, isPublic, groupId } = betData; + + if (!title || title.trim() === '') { + throw new ValidationError('Bet title is required'); + } + + if (!stake || stake.trim() === '') { + throw new ValidationError('Bet stake is required'); + } + + if (!participants || !Array.isArray(participants) || participants.length === 0) { + throw new ValidationError('At least one participant is required'); + } + + const normalizedParticipants = participants.map((p) => ({ + ...p, + user: parseInt(p.user, 10), + })); + + const userIds = normalizedParticipants.map((p) => p.user); + for (const userId of userIds) { + await this.usersRepository.findById(userId); + } + + const bet = await this.betsRepository.create({ + title, + description, + stake, + participants: normalizedParticipants, + isPublic: isPublic || false, + groupId: groupId || null, + }); + + const populated = await this.dataStitchingService.populateBetsWithUsers([bet]); + return populated[0]; + } + + async updateBet(betId, updateData) { + if (updateData.title && updateData.title.trim() === '') { + throw new ValidationError('Bet title cannot be empty'); + } + + const bet = await this.betsRepository.update(betId, updateData); + const populated = await this.dataStitchingService.populateBetsWithUsers([bet]); + return populated[0]; + } + + async deleteBet(betId) { + return await this.betsRepository.delete(betId); + } + + async declareWinner(betId, winnerId) { + await this.usersRepository.findById(winnerId); + + const bet = await this.betsRepository.update(betId, { + winner: winnerId, + status: config.BET_STATUS.COMPLETED, + endDate: new Date(), + }); + + const populated = await this.dataStitchingService.populateBetsWithUsers([bet]); + return populated[0]; + } +} + +module.exports = BetsService; diff --git a/backend-node/services/data-stitching.service.js b/backend-node/services/data-stitching.service.js new file mode 100644 index 0000000..71591df --- /dev/null +++ b/backend-node/services/data-stitching.service.js @@ -0,0 +1,41 @@ +class DataStitchingService { + constructor({ usersRepository }) { + this.usersRepository = usersRepository; + } + + async populateBetsWithUsers(bets) { + if (!bets || bets.length === 0) return []; + + const userIds = new Set(); + bets.forEach((bet) => { + bet.participants.forEach((p) => userIds.add(p.user)); + if (bet.winner) { + userIds.add(bet.winner); + } + }); + + if (userIds.size === 0) + return bets.map((b) => (typeof b.toJSON === 'function' ? b.toJSON() : b)); + + const users = await this.usersRepository.findAll({ + where: { id: Array.from(userIds) }, + }); + const userMap = new Map( + users.map((u) => [u.id, typeof u.toJSON === 'function' ? u.toJSON() : u]) + ); + + return bets.map((bet) => { + const betJson = typeof bet.toJSON === 'function' ? bet.toJSON() : bet; + betJson.participants = betJson.participants.map((p) => ({ + ...p, + user: userMap.get(p.user) || null, + })); + if (betJson.winner) { + betJson.winner = userMap.get(betJson.winner) || null; + } + return betJson; + }); + } +} + +module.exports = DataStitchingService; diff --git a/backend-node/services/friends.service.js b/backend-node/services/friends.service.js new file mode 100644 index 0000000..16af166 --- /dev/null +++ b/backend-node/services/friends.service.js @@ -0,0 +1,102 @@ +const { ValidationError, ConflictError } = require('../shared/errors'); +const { Op } = require('sequelize'); + +class FriendsService { + constructor({ + friendshipsRepository, + friendRequestsRepository, + usersRepository, + sequelize, + }) { + this.friendshipsRepository = friendshipsRepository; + this.friendRequestsRepository = friendRequestsRepository; + this.usersRepository = usersRepository; + this.sequelize = sequelize; + } + + async getFriendsByUserId(userId) { + return await this.friendshipsRepository.findByUserId(userId); + } + + async getPendingRequests(userId) { + return await this.friendRequestsRepository.findPendingByUserId(userId); + } + + async sendFriendRequest(fromUserId, identifier) { + let toUser; + + if (typeof identifier === 'number' || !isNaN(identifier)) { + toUser = await this.usersRepository.findById(parseInt(identifier, 10)); + } else { + toUser = await this.usersRepository.findOne({ + [Op.or]: [{ name: identifier }, { email: identifier }], + }); + + if (!toUser) { + throw new ValidationError( + 'User not found with provided name or email' + ); + } + } + + if (toUser.id === fromUserId) { + throw new ValidationError('Cannot send friend request to yourself'); + } + + const friendshipExists = await this.friendshipsRepository.exists( + fromUserId, + toUser.id + ); + if (friendshipExists) { + throw new ConflictError('You are already friends with this user'); + } + + const existingRequest = await this.friendRequestsRepository.findExisting( + fromUserId, + toUser.id + ); + if (existingRequest) { + throw new ConflictError('Friend request already exists'); + } + + return await this.friendRequestsRepository.create(fromUserId, toUser.id); + } + + async acceptFriendRequest(requestId, userId) { + const request = await this.friendRequestsRepository.findById(requestId); + + if (request.toUserId !== userId) { + throw new ValidationError('You can only accept requests sent to you'); + } + + if (request.status !== 'PENDING') { + throw new ValidationError('This request has already been processed'); + } + + const t = await this.sequelize.transaction(); + try { + await this.friendRequestsRepository.updateStatus(requestId, 'ACCEPTED'); + await this.friendshipsRepository.create(request.fromUserId, request.toUserId); + await this.friendRequestsRepository.delete(requestId); + await t.commit(); + + return { message: 'Friend request accepted successfully' }; + } catch (error) { + await t.rollback(); + throw error; + } + } + + async rejectFriendRequest(requestId, userId) { + const request = await this.friendRequestsRepository.findById(requestId); + + if (request.toUserId !== userId && request.fromUserId !== userId) { + throw new ValidationError('You can only manage your own requests'); + } + + await this.friendRequestsRepository.delete(requestId); + return { message: 'Friend request rejected successfully' }; + } +} + +module.exports = FriendsService; diff --git a/backend-node/services/groups.service.js b/backend-node/services/groups.service.js new file mode 100644 index 0000000..613a222 --- /dev/null +++ b/backend-node/services/groups.service.js @@ -0,0 +1,76 @@ +const { ValidationError } = require('../shared/errors'); + +class GroupsService { + constructor({ groupsRepository, usersRepository }) { + this.groupsRepository = groupsRepository; + this.usersRepository = usersRepository; + } + + async getAllGroups() { + return await this.groupsRepository.findAll({ + include: [ + { + model: this.usersRepository.User, + as: 'members', + through: { attributes: [] }, + }, + ], + }); + } + + async getGroupById(groupId) { + return await this.groupsRepository.findById(groupId, true); + } + + async createGroup(groupData) { + const { name, members } = groupData; + + if (!name || name.trim() === '') { + throw new ValidationError('Group name is required'); + } + + if (!members || !Array.isArray(members) || members.length === 0) { + throw new ValidationError('At least one member is required'); + } + + for (const userId of members) { + await this.usersRepository.findById(userId); + } + + const group = await this.groupsRepository.create({ name }); + return await this.groupsRepository.addMembers(group.id, members); + } + + async updateGroup(groupId, updateData) { + const { name } = updateData; + + if (name && name.trim() === '') { + throw new ValidationError('Group name cannot be empty'); + } + + return await this.groupsRepository.update(groupId, { name }); + } + + async deleteGroup(groupId) { + return await this.groupsRepository.delete(groupId); + } + + async addMembersToGroup(groupId, userIds) { + if (!userIds || !Array.isArray(userIds) || userIds.length === 0) { + throw new ValidationError('User IDs are required'); + } + + for (const userId of userIds) { + await this.usersRepository.findById(userId); + } + + return await this.groupsRepository.addMembers(groupId, userIds); + } + + async removeMemberFromGroup(groupId, userId) { + await this.usersRepository.findById(userId); + return await this.groupsRepository.removeMember(groupId, userId); + } +} + +module.exports = GroupsService; diff --git a/backend-node/services/users.service.js b/backend-node/services/users.service.js new file mode 100644 index 0000000..5554578 --- /dev/null +++ b/backend-node/services/users.service.js @@ -0,0 +1,36 @@ +const { ValidationError } = require('../shared/errors'); + +class UsersService { + constructor({ usersRepository }) { + this.usersRepository = usersRepository; + } + + async getAllUsers() { + return await this.usersRepository.findAll(); + } + + async getUserById(id) { + return await this.usersRepository.findById(id); + } + + async updateUserProfile(id, updateData) { + const { name, email, avatarUrl } = updateData; + + if (name && name.trim() === '') { + throw new ValidationError('Name cannot be empty'); + } + + if (email && email.trim() === '') { + throw new ValidationError('Email cannot be empty'); + } + + const dataToUpdate = {}; + if (name !== undefined) dataToUpdate.name = name; + if (email !== undefined) dataToUpdate.email = email; + if (avatarUrl !== undefined) dataToUpdate.avatarUrl = avatarUrl; + + return await this.usersRepository.update(id, dataToUpdate); + } +} + +module.exports = UsersService; diff --git a/backend-node/shared/errors/AppError.js b/backend-node/shared/errors/AppError.js new file mode 100644 index 0000000..f0d06ee --- /dev/null +++ b/backend-node/shared/errors/AppError.js @@ -0,0 +1,11 @@ +class AppError extends Error { + constructor(message, statusCode, isOperational = true, errors = null) { + super(message); + this.statusCode = statusCode; + this.isOperational = isOperational; + this.errors = errors; + Error.captureStackTrace(this, this.constructor); + } +} + +module.exports = AppError; diff --git a/backend-node/shared/errors/index.js b/backend-node/shared/errors/index.js new file mode 100644 index 0000000..a618253 --- /dev/null +++ b/backend-node/shared/errors/index.js @@ -0,0 +1,55 @@ +const AppError = require('./AppError'); +const config = require('../../config'); + +class ValidationError extends AppError { + constructor(message, errors = null) { + super(message, config.HTTP_STATUS.BAD_REQUEST, true, errors); + this.name = 'ValidationError'; + } +} + +class NotFoundError extends AppError { + constructor(message = 'Resource not found') { + super(message, config.HTTP_STATUS.NOT_FOUND, true); + this.name = 'NotFoundError'; + } +} + +class ConflictError extends AppError { + constructor(message, errors = null) { + super(message, config.HTTP_STATUS.CONFLICT, true, errors); + this.name = 'ConflictError'; + } +} + +class UnauthorizedError extends AppError { + constructor(message = 'Unauthorized') { + super(message, config.HTTP_STATUS.UNAUTHORIZED, true); + this.name = 'UnauthorizedError'; + } +} + +class ForbiddenError extends AppError { + constructor(message = 'Forbidden') { + super(message, config.HTTP_STATUS.FORBIDDEN, true); + this.name = 'ForbiddenError'; + } +} + +class DatabaseError extends AppError { + constructor(message = 'Database operation failed', originalError = null) { + super(message, config.HTTP_STATUS.INTERNAL_SERVER_ERROR, false); + this.name = 'DatabaseError'; + this.originalError = originalError; + } +} + +module.exports = { + AppError, + ValidationError, + NotFoundError, + ConflictError, + UnauthorizedError, + ForbiddenError, + DatabaseError, +}; diff --git a/backend-node/shared/response.handler.js b/backend-node/shared/response.handler.js new file mode 100644 index 0000000..a9f582f --- /dev/null +++ b/backend-node/shared/response.handler.js @@ -0,0 +1,75 @@ +const config = require('../config'); + +class ApiResponse { + constructor(success, statusCode, data = null, message = null, errors = null) { + this.success = success; + this.statusCode = statusCode; + this.data = data; + this.message = message; + this.errors = errors; + this.timestamp = new Date().toISOString(); + } + + toJSON() { + const response = { + success: this.success, + timestamp: this.timestamp, + }; + + if (this.message) response.message = this.message; + if (this.data !== null) response.data = this.data; + if (this.errors) response.errors = this.errors; + + return response; + } +} + +class ResponseHandler { + static success(res, data, message = 'Success', statusCode = config.HTTP_STATUS.OK) { + const response = new ApiResponse(true, statusCode, data, message); + return res.status(statusCode).json(response.toJSON()); + } + + static created(res, data, message = 'Resource created successfully') { + return this.success(res, data, message, config.HTTP_STATUS.CREATED); + } + + static error( + res, + message, + statusCode = config.HTTP_STATUS.INTERNAL_SERVER_ERROR, + errors = null + ) { + const response = new ApiResponse(false, statusCode, null, message, errors); + return res.status(statusCode).json(response.toJSON()); + } + + static badRequest(res, message = 'Bad request', errors = null) { + return this.error(res, message, config.HTTP_STATUS.BAD_REQUEST, errors); + } + + static notFound(res, message = 'Resource not found') { + return this.error(res, message, config.HTTP_STATUS.NOT_FOUND); + } + + static unauthorized(res, message = 'Unauthorized') { + return this.error(res, message, config.HTTP_STATUS.UNAUTHORIZED); + } + + static forbidden(res, message = 'Forbidden') { + return this.error(res, message, config.HTTP_STATUS.FORBIDDEN); + } + + static conflict(res, message = 'Resource conflict', errors = null) { + return this.error(res, message, config.HTTP_STATUS.CONFLICT, errors); + } + + static internalError(res, message = 'Internal server error', error = null) { + if (error && config.isDevelopment) { + console.error('Internal Error:', error); + } + return this.error(res, message, config.HTTP_STATUS.INTERNAL_SERVER_ERROR); + } +} + +module.exports = ResponseHandler; diff --git a/backend-node/shared/validators/schemas/bet.schema.js b/backend-node/shared/validators/schemas/bet.schema.js new file mode 100644 index 0000000..5da7291 --- /dev/null +++ b/backend-node/shared/validators/schemas/bet.schema.js @@ -0,0 +1,49 @@ +const Joi = require('joi'); +const config = require('../../../config'); + +const participantSchema = Joi.object({ + user: Joi.number().integer().positive().required(), + role: Joi.string() + .valid(...Object.values(config.PARTICIPANT_ROLES)) + .required(), +}); + +const createBetSchema = Joi.object({ + title: Joi.string().trim().min(1).max(200).required().messages({ + 'string.empty': 'Bet title is required', + 'string.max': 'Bet title must not exceed 200 characters', + }), + description: Joi.string().trim().max(1000).optional().allow(''), + stake: Joi.string().trim().min(1).required().messages({ + 'string.empty': 'Bet stake is required', + }), + participants: Joi.array().items(participantSchema).min(1).required().messages({ + 'array.min': 'At least one participant is required', + }), + isPublic: Joi.boolean().optional().default(false), + groupId: Joi.string().optional().allow(null), +}); + +const updateBetSchema = Joi.object({ + title: Joi.string().trim().min(1).max(200).optional(), + description: Joi.string().trim().max(1000).optional().allow(''), + stake: Joi.string().trim().min(1).optional(), + status: Joi.string() + .valid(...Object.values(config.BET_STATUS)) + .optional(), + endDate: Joi.date().optional(), + winner: Joi.number().integer().positive().optional().allow(null), +}); + +const declareWinnerSchema = Joi.object({ + winnerId: Joi.number().integer().positive().required().messages({ + 'number.base': 'Winner ID must be a number', + 'any.required': 'Winner ID is required', + }), +}); + +module.exports = { + createBetSchema, + updateBetSchema, + declareWinnerSchema, +}; diff --git a/backend-node/shared/validators/schemas/friendRequest.schema.js b/backend-node/shared/validators/schemas/friendRequest.schema.js new file mode 100644 index 0000000..9c3169b --- /dev/null +++ b/backend-node/shared/validators/schemas/friendRequest.schema.js @@ -0,0 +1,11 @@ +const Joi = require('joi'); + +const sendFriendRequestSchema = Joi.object({ + identifier: Joi.alternatives().try(Joi.number().integer().positive(), Joi.string().min(1)).required().messages({ + 'any.required': 'User identifier (ID, name, or email) is required', + }), +}); + +module.exports = { + sendFriendRequestSchema, +}; diff --git a/backend-node/shared/validators/schemas/group.schema.js b/backend-node/shared/validators/schemas/group.schema.js new file mode 100644 index 0000000..78b751e --- /dev/null +++ b/backend-node/shared/validators/schemas/group.schema.js @@ -0,0 +1,29 @@ +const Joi = require('joi'); + +const createGroupSchema = Joi.object({ + name: Joi.string().trim().min(1).max(100).required().messages({ + 'string.empty': 'Group name is required', + 'string.min': 'Group name must be at least 1 character', + 'string.max': 'Group name must not exceed 100 characters', + }), + members: Joi.array().items(Joi.number().integer().positive()).min(1).required().messages({ + 'array.min': 'At least one member is required', + 'array.base': 'Members must be an array', + }), +}); + +const updateGroupSchema = Joi.object({ + name: Joi.string().trim().min(1).max(100).optional(), +}); + +const addMembersSchema = Joi.object({ + userIds: Joi.array().items(Joi.number().integer().positive()).min(1).required().messages({ + 'array.min': 'At least one user ID is required', + }), +}); + +module.exports = { + createGroupSchema, + updateGroupSchema, + addMembersSchema, +}; diff --git a/backend-node/shared/validators/schemas/user.schema.js b/backend-node/shared/validators/schemas/user.schema.js new file mode 100644 index 0000000..89fa48d --- /dev/null +++ b/backend-node/shared/validators/schemas/user.schema.js @@ -0,0 +1,11 @@ +const Joi = require('joi'); + +const updateUserSchema = Joi.object({ + name: Joi.string().trim().min(1).max(100).optional(), + email: Joi.string().email().optional(), + avatarUrl: Joi.string().uri().optional().allow('', null), +}); + +module.exports = { + updateUserSchema, +}; diff --git a/backend-node/utils/populateBets.js b/backend-node/utils/populateBets.js new file mode 100644 index 0000000..2582e10 --- /dev/null +++ b/backend-node/utils/populateBets.js @@ -0,0 +1,43 @@ +// Questo helper necessita del modello User da Sequelize +module.exports = (sequelize) => { + const { User } = sequelize.models; + + /** + * Funzione helper per arricchire i documenti delle scommesse da MongoDB con i dati utente da PostgreSQL. + * @param {Array} bets - Un array di documenti di scommesse Mongoose. + * @returns {Array} Un array di oggetti scommessa con i dettagli utente popolati. + */ + const populateBetsWithUsers = async (bets) => { + if (!bets || bets.length === 0) return []; + + // Raccoglie tutti gli ID utente unici da tutte le scommesse + const userIds = new Set(); + bets.forEach(bet => { + bet.participants.forEach(p => userIds.add(p.user)); + if (bet.winner) { + userIds.add(bet.winner); + } + }); + + if (userIds.size === 0) return bets.map(b => b.toJSON()); + + // Recupera tutti gli utenti necessari da PostgreSQL con una singola query + const users = await User.findAll({ where: { id: Array.from(userIds) } }); + const userMap = new Map(users.map(u => [u.id, u.toJSON()])); + + // Unisce i dati utente negli oggetti scommessa + return bets.map(bet => { + const betJson = bet.toJSON(); + betJson.participants = betJson.participants.map(p => ({ + ...p, + user: userMap.get(p.user) || null + })); + if (betJson.winner) { + betJson.winner = userMap.get(betJson.winner) || null; + } + return betJson; + }); + }; + + return { populateBetsWithUsers }; +}; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7b4bbbe --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,100 @@ +version: '3.8' + +services: + # PostgreSQL Database + postgres: + image: postgres:15-alpine + container_name: scommetti-postgres + environment: + POSTGRES_DB: amici_scommesse + POSTGRES_USER: admin + POSTGRES_PASSWORD: admin + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./backend-node/init.sql:/docker-entrypoint-initdb.d/init.sql + networks: + - scommetti-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U admin -d amici_scommesse"] + interval: 10s + timeout: 5s + retries: 5 + + # MongoDB Database + mongodb: + image: mongo:7 + container_name: scommetti-mongodb + environment: + MONGO_INITDB_ROOT_USERNAME: root + MONGO_INITDB_ROOT_PASSWORD: root@servovio + MONGO_INITDB_DATABASE: amici-scommesse + ports: + - "27017:27017" + volumes: + - mongodb_data:/data/db + networks: + - scommetti-network + healthcheck: + test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"] + interval: 10s + timeout: 5s + retries: 5 + + # Backend Node.js + backend: + build: + context: ./backend-node + dockerfile: Dockerfile + container_name: scommetti-backend + environment: + NODE_ENV: development + PORT: 3000 + POSTGRES_URI: postgres://admin:admin@postgres:5432/amici_scommesse + MONGO_URI: mongodb://mongodb:27017/amici-scommesse + ports: + - "3000:3000" + volumes: + - ./backend-node:/app + - /app/node_modules + depends_on: + postgres: + condition: service_healthy + mongodb: + condition: service_healthy + networks: + - scommetti-network + command: npm start + restart: unless-stopped + + # Frontend Angular + frontend: + build: + context: ./frontend-angular + dockerfile: Dockerfile + container_name: scommetti-frontend + environment: + NODE_ENV: development + ports: + - "4200:4200" + volumes: + - ./frontend-angular:/app + - /app/node_modules + - /app/.angular + depends_on: + - backend + networks: + - scommetti-network + command: npm run dev + restart: unless-stopped + +volumes: + postgres_data: + driver: local + mongodb_data: + driver: local + +networks: + scommetti-network: + driver: bridge diff --git a/frontend-flutter/.dart_tool/package_config.json b/frontend-flutter/.dart_tool/package_config.json new file mode 100644 index 0000000..6ed495c --- /dev/null +++ b/frontend-flutter/.dart_tool/package_config.json @@ -0,0 +1,460 @@ +{ + "configVersion": 2, + "packages": [ + { + "name": "_fe_analyzer_shared", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/_fe_analyzer_shared-91.0.0", + "packageUri": "lib/", + "languageVersion": "3.9" + }, + { + "name": "analyzer", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/analyzer-8.4.1", + "packageUri": "lib/", + "languageVersion": "3.9" + }, + { + "name": "args", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/args-2.7.0", + "packageUri": "lib/", + "languageVersion": "3.3" + }, + { + "name": "async", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/async-2.13.0", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "bloc", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/bloc-8.1.4", + "packageUri": "lib/", + "languageVersion": "2.12" + }, + { + "name": "bloc_test", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/bloc_test-9.1.7", + "packageUri": "lib/", + "languageVersion": "2.12" + }, + { + "name": "boolean_selector", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "characters", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/characters-1.4.0", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "cli_config", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/cli_config-0.2.0", + "packageUri": "lib/", + "languageVersion": "3.0" + }, + { + "name": "clock", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/clock-1.1.2", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "collection", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/collection-1.19.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "convert", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/convert-3.1.2", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "coverage", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/coverage-1.15.0", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "crypto", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/crypto-3.0.7", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "cupertino_icons", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/cupertino_icons-1.0.8", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "diff_match_patch", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/diff_match_patch-0.4.1", + "packageUri": "lib/", + "languageVersion": "2.12" + }, + { + "name": "equatable", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/equatable-2.0.8", + "packageUri": "lib/", + "languageVersion": "2.12" + }, + { + "name": "fake_async", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/fake_async-1.3.3", + "packageUri": "lib/", + "languageVersion": "3.3" + }, + { + "name": "file", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/file-7.0.1", + "packageUri": "lib/", + "languageVersion": "3.0" + }, + { + "name": "flutter", + "rootUri": "file:///root/development/flutter/packages/flutter", + "packageUri": "lib/", + "languageVersion": "3.8" + }, + { + "name": "flutter_bloc", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/flutter_bloc-8.1.6", + "packageUri": "lib/", + "languageVersion": "2.12" + }, + { + "name": "flutter_lints", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/flutter_lints-4.0.0", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "flutter_test", + "rootUri": "file:///root/development/flutter/packages/flutter_test", + "packageUri": "lib/", + "languageVersion": "3.8" + }, + { + "name": "flutter_web_plugins", + "rootUri": "file:///root/development/flutter/packages/flutter_web_plugins", + "packageUri": "lib/", + "languageVersion": "3.8" + }, + { + "name": "frontend_server_client", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/frontend_server_client-4.0.0", + "packageUri": "lib/", + "languageVersion": "3.0" + }, + { + "name": "glob", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/glob-2.1.3", + "packageUri": "lib/", + "languageVersion": "3.3" + }, + { + "name": "go_router", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/go_router-14.8.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "http", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/http-1.6.0", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "http_multi_server", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/http_multi_server-3.2.2", + "packageUri": "lib/", + "languageVersion": "3.2" + }, + { + "name": "http_parser", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/http_parser-4.1.2", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "intl", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/intl-0.19.0", + "packageUri": "lib/", + "languageVersion": "3.0" + }, + { + "name": "io", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/io-1.0.5", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "js", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/js-0.7.2", + "packageUri": "lib/", + "languageVersion": "3.7" + }, + { + "name": "leak_tracker", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/leak_tracker-11.0.2", + "packageUri": "lib/", + "languageVersion": "3.2" + }, + { + "name": "leak_tracker_flutter_testing", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/leak_tracker_flutter_testing-3.0.10", + "packageUri": "lib/", + "languageVersion": "3.2" + }, + { + "name": "leak_tracker_testing", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/leak_tracker_testing-3.0.2", + "packageUri": "lib/", + "languageVersion": "3.2" + }, + { + "name": "lints", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/lints-4.0.0", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "logging", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/logging-1.3.0", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "matcher", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/matcher-0.12.17", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "material_color_utilities", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/material_color_utilities-0.11.1", + "packageUri": "lib/", + "languageVersion": "2.17" + }, + { + "name": "meta", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/meta-1.17.0", + "packageUri": "lib/", + "languageVersion": "3.5" + }, + { + "name": "mime", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/mime-2.0.0", + "packageUri": "lib/", + "languageVersion": "3.2" + }, + { + "name": "mocktail", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/mocktail-1.0.4", + "packageUri": "lib/", + "languageVersion": "2.12" + }, + { + "name": "nested", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/nested-1.0.0", + "packageUri": "lib/", + "languageVersion": "2.12" + }, + { + "name": "node_preamble", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/node_preamble-2.0.2", + "packageUri": "lib/", + "languageVersion": "2.12" + }, + { + "name": "package_config", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/package_config-2.2.0", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "path", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/path-1.9.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "pool", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/pool-1.5.2", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "provider", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/provider-6.1.5+1", + "packageUri": "lib/", + "languageVersion": "2.12" + }, + { + "name": "pub_semver", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/pub_semver-2.2.0", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "shelf", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/shelf-1.4.2", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "shelf_packages_handler", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/shelf_packages_handler-3.0.2", + "packageUri": "lib/", + "languageVersion": "2.17" + }, + { + "name": "shelf_static", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/shelf_static-1.1.3", + "packageUri": "lib/", + "languageVersion": "3.3" + }, + { + "name": "shelf_web_socket", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/shelf_web_socket-3.0.0", + "packageUri": "lib/", + "languageVersion": "3.5" + }, + { + "name": "sky_engine", + "rootUri": "file:///root/development/flutter/bin/cache/pkg/sky_engine", + "packageUri": "lib/", + "languageVersion": "3.8" + }, + { + "name": "source_map_stack_trace", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/source_map_stack_trace-2.1.2", + "packageUri": "lib/", + "languageVersion": "3.3" + }, + { + "name": "source_maps", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/source_maps-0.10.13", + "packageUri": "lib/", + "languageVersion": "3.3" + }, + { + "name": "source_span", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/source_span-1.10.1", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "stack_trace", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/stack_trace-1.12.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "stream_channel", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/stream_channel-2.1.4", + "packageUri": "lib/", + "languageVersion": "3.3" + }, + { + "name": "string_scanner", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/string_scanner-1.4.1", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "term_glyph", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/term_glyph-1.2.2", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "test", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/test-1.26.3", + "packageUri": "lib/", + "languageVersion": "3.5" + }, + { + "name": "test_api", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/test_api-0.7.7", + "packageUri": "lib/", + "languageVersion": "3.5" + }, + { + "name": "test_core", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/test_core-0.6.12", + "packageUri": "lib/", + "languageVersion": "3.5" + }, + { + "name": "typed_data", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/typed_data-1.4.0", + "packageUri": "lib/", + "languageVersion": "3.5" + }, + { + "name": "vector_math", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/vector_math-2.2.0", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "vm_service", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/vm_service-15.0.2", + "packageUri": "lib/", + "languageVersion": "3.5" + }, + { + "name": "watcher", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/watcher-1.2.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "web", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/web-1.1.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "web_socket", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/web_socket-1.0.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "web_socket_channel", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/web_socket_channel-3.0.3", + "packageUri": "lib/", + "languageVersion": "3.3" + }, + { + "name": "webkit_inspection_protocol", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/webkit_inspection_protocol-1.2.1", + "packageUri": "lib/", + "languageVersion": "3.0" + }, + { + "name": "yaml", + "rootUri": "file:///root/.pub-cache/hosted/pub.dev/yaml-3.1.3", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "scomettiapp", + "rootUri": "../", + "packageUri": "lib/", + "languageVersion": "3.5" + } + ], + "generator": "pub", + "generatorVersion": "3.10.8", + "flutterRoot": "file:///root/development/flutter", + "flutterVersion": "3.38.9", + "pubCache": "file:///root/.pub-cache" +} diff --git a/frontend-flutter/.dart_tool/package_graph.json b/frontend-flutter/.dart_tool/package_graph.json new file mode 100644 index 0000000..9a8ffad --- /dev/null +++ b/frontend-flutter/.dart_tool/package_graph.json @@ -0,0 +1,674 @@ +{ + "roots": [ + "scomettiapp" + ], + "packages": [ + { + "name": "scomettiapp", + "version": "1.0.0+1", + "dependencies": [ + "bloc", + "cupertino_icons", + "equatable", + "flutter", + "flutter_bloc", + "go_router", + "http", + "intl" + ], + "devDependencies": [ + "bloc_test", + "flutter_lints", + "flutter_test", + "mocktail" + ] + }, + { + "name": "mocktail", + "version": "1.0.4", + "dependencies": [ + "collection", + "matcher", + "test_api" + ] + }, + { + "name": "bloc_test", + "version": "9.1.7", + "dependencies": [ + "bloc", + "diff_match_patch", + "meta", + "mocktail", + "test" + ] + }, + { + "name": "flutter_lints", + "version": "4.0.0", + "dependencies": [ + "lints" + ] + }, + { + "name": "flutter_test", + "version": "0.0.0", + "dependencies": [ + "clock", + "collection", + "fake_async", + "flutter", + "leak_tracker_flutter_testing", + "matcher", + "meta", + "path", + "stack_trace", + "stream_channel", + "test_api", + "vector_math" + ] + }, + { + "name": "intl", + "version": "0.19.0", + "dependencies": [ + "clock", + "meta", + "path" + ] + }, + { + "name": "cupertino_icons", + "version": "1.0.8", + "dependencies": [] + }, + { + "name": "bloc", + "version": "8.1.4", + "dependencies": [ + "meta" + ] + }, + { + "name": "flutter_bloc", + "version": "8.1.6", + "dependencies": [ + "bloc", + "flutter", + "provider" + ] + }, + { + "name": "flutter", + "version": "0.0.0", + "dependencies": [ + "characters", + "collection", + "material_color_utilities", + "meta", + "sky_engine", + "vector_math" + ] + }, + { + "name": "test_api", + "version": "0.7.7", + "dependencies": [ + "async", + "boolean_selector", + "collection", + "meta", + "source_span", + "stack_trace", + "stream_channel", + "string_scanner", + "term_glyph" + ] + }, + { + "name": "matcher", + "version": "0.12.17", + "dependencies": [ + "async", + "meta", + "stack_trace", + "term_glyph", + "test_api" + ] + }, + { + "name": "collection", + "version": "1.19.1", + "dependencies": [] + }, + { + "name": "meta", + "version": "1.17.0", + "dependencies": [] + }, + { + "name": "diff_match_patch", + "version": "0.4.1", + "dependencies": [] + }, + { + "name": "lints", + "version": "4.0.0", + "dependencies": [] + }, + { + "name": "vector_math", + "version": "2.2.0", + "dependencies": [] + }, + { + "name": "stream_channel", + "version": "2.1.4", + "dependencies": [ + "async" + ] + }, + { + "name": "stack_trace", + "version": "1.12.1", + "dependencies": [ + "path" + ] + }, + { + "name": "path", + "version": "1.9.1", + "dependencies": [] + }, + { + "name": "leak_tracker_flutter_testing", + "version": "3.0.10", + "dependencies": [ + "flutter", + "leak_tracker", + "leak_tracker_testing", + "matcher", + "meta" + ] + }, + { + "name": "fake_async", + "version": "1.3.3", + "dependencies": [ + "clock", + "collection" + ] + }, + { + "name": "clock", + "version": "1.1.2", + "dependencies": [] + }, + { + "name": "sky_engine", + "version": "0.0.0", + "dependencies": [] + }, + { + "name": "material_color_utilities", + "version": "0.11.1", + "dependencies": [ + "collection" + ] + }, + { + "name": "characters", + "version": "1.4.0", + "dependencies": [] + }, + { + "name": "leak_tracker_testing", + "version": "3.0.2", + "dependencies": [ + "leak_tracker", + "matcher", + "meta" + ] + }, + { + "name": "leak_tracker", + "version": "11.0.2", + "dependencies": [ + "clock", + "collection", + "meta", + "path", + "vm_service" + ] + }, + { + "name": "term_glyph", + "version": "1.2.2", + "dependencies": [] + }, + { + "name": "boolean_selector", + "version": "2.1.2", + "dependencies": [ + "source_span", + "string_scanner" + ] + }, + { + "name": "equatable", + "version": "2.0.8", + "dependencies": [ + "collection", + "meta" + ] + }, + { + "name": "async", + "version": "2.13.0", + "dependencies": [ + "collection", + "meta" + ] + }, + { + "name": "string_scanner", + "version": "1.4.1", + "dependencies": [ + "source_span" + ] + }, + { + "name": "go_router", + "version": "14.8.1", + "dependencies": [ + "collection", + "flutter", + "flutter_web_plugins", + "logging", + "meta" + ] + }, + { + "name": "flutter_web_plugins", + "version": "0.0.0", + "dependencies": [ + "flutter" + ] + }, + { + "name": "http", + "version": "1.6.0", + "dependencies": [ + "async", + "http_parser", + "meta", + "web" + ] + }, + { + "name": "web", + "version": "1.1.1", + "dependencies": [] + }, + { + "name": "http_parser", + "version": "4.1.2", + "dependencies": [ + "collection", + "source_span", + "string_scanner", + "typed_data" + ] + }, + { + "name": "typed_data", + "version": "1.4.0", + "dependencies": [ + "collection" + ] + }, + { + "name": "source_span", + "version": "1.10.1", + "dependencies": [ + "collection", + "path", + "term_glyph" + ] + }, + { + "name": "logging", + "version": "1.3.0", + "dependencies": [] + }, + { + "name": "provider", + "version": "6.1.5+1", + "dependencies": [ + "collection", + "flutter", + "nested" + ] + }, + { + "name": "nested", + "version": "1.0.0", + "dependencies": [ + "flutter" + ] + }, + { + "name": "vm_service", + "version": "15.0.2", + "dependencies": [] + }, + { + "name": "pool", + "version": "1.5.2", + "dependencies": [ + "async", + "stack_trace" + ] + }, + { + "name": "test", + "version": "1.26.3", + "dependencies": [ + "analyzer", + "async", + "boolean_selector", + "collection", + "coverage", + "http_multi_server", + "io", + "js", + "matcher", + "node_preamble", + "package_config", + "path", + "pool", + "shelf", + "shelf_packages_handler", + "shelf_static", + "shelf_web_socket", + "source_span", + "stack_trace", + "stream_channel", + "test_api", + "test_core", + "typed_data", + "web_socket_channel", + "webkit_inspection_protocol", + "yaml" + ] + }, + { + "name": "test_core", + "version": "0.6.12", + "dependencies": [ + "analyzer", + "args", + "async", + "boolean_selector", + "collection", + "coverage", + "frontend_server_client", + "glob", + "io", + "meta", + "package_config", + "path", + "pool", + "source_map_stack_trace", + "source_maps", + "source_span", + "stack_trace", + "stream_channel", + "test_api", + "vm_service", + "yaml" + ] + }, + { + "name": "frontend_server_client", + "version": "4.0.0", + "dependencies": [ + "async", + "path" + ] + }, + { + "name": "shelf_packages_handler", + "version": "3.0.2", + "dependencies": [ + "path", + "shelf", + "shelf_static" + ] + }, + { + "name": "node_preamble", + "version": "2.0.2", + "dependencies": [] + }, + { + "name": "source_map_stack_trace", + "version": "2.1.2", + "dependencies": [ + "path", + "source_maps", + "stack_trace" + ] + }, + { + "name": "source_maps", + "version": "0.10.13", + "dependencies": [ + "source_span" + ] + }, + { + "name": "yaml", + "version": "3.1.3", + "dependencies": [ + "collection", + "source_span", + "string_scanner" + ] + }, + { + "name": "webkit_inspection_protocol", + "version": "1.2.1", + "dependencies": [ + "logging" + ] + }, + { + "name": "shelf_static", + "version": "1.1.3", + "dependencies": [ + "convert", + "http_parser", + "mime", + "path", + "shelf" + ] + }, + { + "name": "package_config", + "version": "2.2.0", + "dependencies": [ + "path" + ] + }, + { + "name": "io", + "version": "1.0.5", + "dependencies": [ + "meta", + "path", + "string_scanner" + ] + }, + { + "name": "http_multi_server", + "version": "3.2.2", + "dependencies": [ + "async" + ] + }, + { + "name": "convert", + "version": "3.1.2", + "dependencies": [ + "typed_data" + ] + }, + { + "name": "js", + "version": "0.7.2", + "dependencies": [] + }, + { + "name": "glob", + "version": "2.1.3", + "dependencies": [ + "async", + "collection", + "file", + "path", + "string_scanner" + ] + }, + { + "name": "file", + "version": "7.0.1", + "dependencies": [ + "meta", + "path" + ] + }, + { + "name": "shelf_web_socket", + "version": "3.0.0", + "dependencies": [ + "shelf", + "stream_channel", + "web_socket_channel" + ] + }, + { + "name": "mime", + "version": "2.0.0", + "dependencies": [] + }, + { + "name": "shelf", + "version": "1.4.2", + "dependencies": [ + "async", + "collection", + "http_parser", + "path", + "stack_trace", + "stream_channel" + ] + }, + { + "name": "web_socket_channel", + "version": "3.0.3", + "dependencies": [ + "async", + "crypto", + "stream_channel", + "web", + "web_socket" + ] + }, + { + "name": "web_socket", + "version": "1.0.1", + "dependencies": [ + "web" + ] + }, + { + "name": "crypto", + "version": "3.0.7", + "dependencies": [ + "typed_data" + ] + }, + { + "name": "args", + "version": "2.7.0", + "dependencies": [] + }, + { + "name": "coverage", + "version": "1.15.0", + "dependencies": [ + "args", + "cli_config", + "glob", + "logging", + "meta", + "package_config", + "path", + "source_maps", + "stack_trace", + "vm_service", + "yaml" + ] + }, + { + "name": "cli_config", + "version": "0.2.0", + "dependencies": [ + "args", + "yaml" + ] + }, + { + "name": "analyzer", + "version": "8.4.1", + "dependencies": [ + "_fe_analyzer_shared", + "collection", + "convert", + "crypto", + "glob", + "meta", + "package_config", + "path", + "pub_semver", + "source_span", + "watcher", + "yaml" + ] + }, + { + "name": "_fe_analyzer_shared", + "version": "91.0.0", + "dependencies": [ + "meta" + ] + }, + { + "name": "pub_semver", + "version": "2.2.0", + "dependencies": [ + "collection" + ] + }, + { + "name": "watcher", + "version": "1.2.1", + "dependencies": [ + "async", + "path" + ] + } + ], + "configVersion": 1 +} \ No newline at end of file diff --git a/frontend-flutter/.dart_tool/version b/frontend-flutter/.dart_tool/version new file mode 100644 index 0000000..75fffa6 --- /dev/null +++ b/frontend-flutter/.dart_tool/version @@ -0,0 +1 @@ +3.38.9 \ No newline at end of file diff --git a/frontend-flutter/lib/core/api/api_client.dart b/frontend-flutter/lib/core/api/api_client.dart new file mode 100644 index 0000000..2bde06d --- /dev/null +++ b/frontend-flutter/lib/core/api/api_client.dart @@ -0,0 +1,190 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:http/http.dart' as http; + +/// Exception thrown when an API request fails +class ApiException implements Exception { + final String message; + final int? statusCode; + + ApiException(this.message, [this.statusCode]); + + @override + String toString() => 'ApiException: $message (Status: $statusCode)'; +} + +/// HTTP API client for making network requests +class ApiClient { + final http.Client _httpClient; + final String baseUrl; + final Duration timeout; + + ApiClient({ + http.Client? httpClient, + required this.baseUrl, + this.timeout = const Duration(seconds: 30), + }) : _httpClient = httpClient ?? http.Client(); + + /// Make a GET request + Future> get( + String endpoint, { + Map? headers, + Map? queryParameters, + }) async { + final uri = _buildUri(endpoint, queryParameters); + + try { + final response = await _httpClient + .get( + uri, + headers: _buildHeaders(headers), + ) + .timeout(timeout); + + return _handleResponse(response); + } on TimeoutException { + throw ApiException('Request timeout'); + } catch (e) { + throw ApiException('Network error: $e'); + } + } + + /// Make a POST request + Future> post( + String endpoint, { + Map? headers, + Map? body, + Map? queryParameters, + }) async { + final uri = _buildUri(endpoint, queryParameters); + + try { + final response = await _httpClient + .post( + uri, + headers: _buildHeaders(headers), + body: body != null ? jsonEncode(body) : null, + ) + .timeout(timeout); + + return _handleResponse(response); + } on TimeoutException { + throw ApiException('Request timeout'); + } catch (e) { + throw ApiException('Network error: $e'); + } + } + + /// Make a PUT request + Future> put( + String endpoint, { + Map? headers, + Map? body, + Map? queryParameters, + }) async { + final uri = _buildUri(endpoint, queryParameters); + + try { + final response = await _httpClient + .put( + uri, + headers: _buildHeaders(headers), + body: body != null ? jsonEncode(body) : null, + ) + .timeout(timeout); + + return _handleResponse(response); + } on TimeoutException { + throw ApiException('Request timeout'); + } catch (e) { + throw ApiException('Network error: $e'); + } + } + + /// Make a DELETE request + Future> delete( + String endpoint, { + Map? headers, + Map? queryParameters, + }) async { + final uri = _buildUri(endpoint, queryParameters); + + try { + final response = await _httpClient + .delete( + uri, + headers: _buildHeaders(headers), + ) + .timeout(timeout); + + return _handleResponse(response); + } on TimeoutException { + throw ApiException('Request timeout'); + } catch (e) { + throw ApiException('Network error: $e'); + } + } + + /// Build URI with query parameters + Uri _buildUri(String endpoint, Map? queryParameters) { + final path = endpoint.startsWith('/') ? endpoint : '/$endpoint'; + final uri = Uri.parse('$baseUrl$path'); + + if (queryParameters != null && queryParameters.isNotEmpty) { + return uri.replace( + queryParameters: queryParameters.map( + (key, value) => MapEntry(key, value.toString()), + ), + ); + } + + return uri; + } + + /// Build request headers + Map _buildHeaders(Map? customHeaders) { + final headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + + if (customHeaders != null) { + headers.addAll(customHeaders); + } + + return headers; + } + + /// Handle HTTP response + Map _handleResponse(http.Response response) { + if (response.statusCode >= 200 && response.statusCode < 300) { + if (response.body.isEmpty) { + return {}; + } + + try { + return jsonDecode(response.body) as Map; + } catch (e) { + throw ApiException('Invalid JSON response'); + } + } + + // Handle error responses + String errorMessage = 'Request failed'; + try { + final errorBody = jsonDecode(response.body); + errorMessage = errorBody['message'] ?? errorBody['error'] ?? errorMessage; + } catch (_) { + errorMessage = response.body.isNotEmpty + ? response.body + : 'Status code: ${response.statusCode}'; + } + + throw ApiException(errorMessage, response.statusCode); + } + + /// Dispose resources + void dispose() { + _httpClient.close(); + } +} diff --git a/frontend-flutter/lib/core/models/bet.dart b/frontend-flutter/lib/core/models/bet.dart new file mode 100644 index 0000000..8b601ce --- /dev/null +++ b/frontend-flutter/lib/core/models/bet.dart @@ -0,0 +1,142 @@ +import 'package:equatable/equatable.dart'; +import 'user.dart'; + +enum BetStatus { + active('ACTIVE'), + awaitingValidation('AWAITING_VALIDATION'), + completed('COMPLETED'), + expired('EXPIRED'); + + final String value; + const BetStatus(this.value); + + static BetStatus fromString(String value) { + return BetStatus.values.firstWhere( + (e) => e.value == value, + orElse: () => BetStatus.active, + ); + } +} + +enum BetRole { + participant('PARTICIPANT'), + observer('OBSERVER'); + + final String value; + const BetRole(this.value); + + static BetRole fromString(String value) { + return BetRole.values.firstWhere( + (e) => e.value == value, + orElse: () => BetRole.observer, + ); + } +} + +class BetParticipant extends Equatable { + final User user; + final BetRole role; + + const BetParticipant({ + required this.user, + required this.role, + }); + + factory BetParticipant.fromJson(Map json) { + return BetParticipant( + user: User.fromJson(json['user'] as Map), + role: BetRole.fromString(json['role'] as String), + ); + } + + Map toJson() { + return { + 'user': user.toJson(), + 'role': role.value, + }; + } + + @override + List get props => [user, role]; +} + +class Bet extends Equatable { + final String id; + final String title; + final String description; + final List participants; + final BetStatus status; + final DateTime startDate; + final DateTime? endDate; + final bool isPublic; + final String? groupId; + final String stake; + final User? winner; + + const Bet({ + required this.id, + required this.title, + required this.description, + required this.participants, + required this.status, + required this.startDate, + this.endDate, + required this.isPublic, + this.groupId, + required this.stake, + this.winner, + }); + + factory Bet.fromJson(Map json) { + return Bet( + id: json['id'] as String, + title: json['title'] as String, + description: json['description'] as String, + participants: (json['participants'] as List) + .map((p) => BetParticipant.fromJson(p as Map)) + .toList(), + status: BetStatus.fromString(json['status'] as String), + startDate: DateTime.parse(json['startDate'] as String), + endDate: json['endDate'] != null + ? DateTime.parse(json['endDate'] as String) + : null, + isPublic: json['isPublic'] as bool, + groupId: json['groupId'] as String?, + stake: json['stake'] as String, + winner: json['winner'] != null + ? User.fromJson(json['winner'] as Map) + : null, + ); + } + + Map toJson() { + return { + 'id': id, + 'title': title, + 'description': description, + 'participants': participants.map((p) => p.toJson()).toList(), + 'status': status.value, + 'startDate': startDate.toIso8601String(), + 'endDate': endDate?.toIso8601String(), + 'isPublic': isPublic, + 'groupId': groupId, + 'stake': stake, + 'winner': winner?.toJson(), + }; + } + + @override + List get props => [ + id, + title, + description, + participants, + status, + startDate, + endDate, + isPublic, + groupId, + stake, + winner, + ]; +} diff --git a/frontend-flutter/lib/core/models/friend_request.dart b/frontend-flutter/lib/core/models/friend_request.dart new file mode 100644 index 0000000..a80c37a --- /dev/null +++ b/frontend-flutter/lib/core/models/friend_request.dart @@ -0,0 +1,55 @@ +import 'package:equatable/equatable.dart'; +import 'user.dart'; + +enum FriendRequestStatus { + pending('PENDING'); + + final String value; + const FriendRequestStatus(this.value); + + static FriendRequestStatus fromString(String value) { + return FriendRequestStatus.values.firstWhere( + (e) => e.value == value, + orElse: () => FriendRequestStatus.pending, + ); + } +} + +class FriendRequest extends Equatable { + final String id; + final User fromUser; + final User toUser; + final FriendRequestStatus status; + final DateTime createdAt; + + const FriendRequest({ + required this.id, + required this.fromUser, + required this.toUser, + required this.status, + required this.createdAt, + }); + + factory FriendRequest.fromJson(Map json) { + return FriendRequest( + id: json['id'] as String, + fromUser: User.fromJson(json['fromUser'] as Map), + toUser: User.fromJson(json['toUser'] as Map), + status: FriendRequestStatus.fromString(json['status'] as String), + createdAt: DateTime.parse(json['createdAt'] as String), + ); + } + + Map toJson() { + return { + 'id': id, + 'fromUser': fromUser.toJson(), + 'toUser': toUser.toJson(), + 'status': status.value, + 'createdAt': createdAt.toIso8601String(), + }; + } + + @override + List get props => [id, fromUser, toUser, status, createdAt]; +} diff --git a/frontend-flutter/lib/core/models/group.dart b/frontend-flutter/lib/core/models/group.dart new file mode 100644 index 0000000..3652185 --- /dev/null +++ b/frontend-flutter/lib/core/models/group.dart @@ -0,0 +1,47 @@ +import 'package:equatable/equatable.dart'; +import 'user.dart'; + +class Group extends Equatable { + final String id; + final String name; + final List members; + + const Group({ + required this.id, + required this.name, + required this.members, + }); + + factory Group.fromJson(Map json) { + return Group( + id: json['id'] as String, + name: json['name'] as String, + members: (json['members'] as List) + .map((m) => User.fromJson(m as Map)) + .toList(), + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'members': members.map((m) => m.toJson()).toList(), + }; + } + + Group copyWith({ + String? id, + String? name, + List? members, + }) { + return Group( + id: id ?? this.id, + name: name ?? this.name, + members: members ?? this.members, + ); + } + + @override + List get props => [id, name, members]; +} diff --git a/frontend-flutter/lib/core/models/user.dart b/frontend-flutter/lib/core/models/user.dart new file mode 100644 index 0000000..9fadcae --- /dev/null +++ b/frontend-flutter/lib/core/models/user.dart @@ -0,0 +1,56 @@ +import 'package:equatable/equatable.dart'; + +class User extends Equatable { + final String id; + final String name; + final String email; + final String avatarUrl; + final double score; + + const User({ + required this.id, + required this.name, + required this.email, + required this.avatarUrl, + required this.score, + }); + + factory User.fromJson(Map json) { + return User( + id: json['id'] as String, + name: json['name'] as String, + email: json['email'] as String, + avatarUrl: json['avatarUrl'] as String, + score: (json['score'] as num).toDouble(), + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'email': email, + 'avatarUrl': avatarUrl, + 'score': score, + }; + } + + User copyWith({ + String? id, + String? name, + String? email, + String? avatarUrl, + double? score, + }) { + return User( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + avatarUrl: avatarUrl ?? this.avatarUrl, + score: score ?? this.score, + ); + } + + @override + List get props => [id, name, email, avatarUrl, score]; +} diff --git a/frontend-flutter/lib/core/navigation/app_router.dart b/frontend-flutter/lib/core/navigation/app_router.dart new file mode 100644 index 0000000..c4aafe4 --- /dev/null +++ b/frontend-flutter/lib/core/navigation/app_router.dart @@ -0,0 +1,154 @@ +import 'package:flutter/material.dart'; + +/// App route paths +class AppRoutes { + static const String home = '/'; + static const String matchDetails = '/match/:id'; + static const String bookmakerDetails = '/bookmaker/:id'; + static const String settings = '/settings'; +} + +/// Route configuration for Navigator 2.0 +class AppRouterDelegate extends RouterDelegate + with ChangeNotifier, PopNavigatorRouterDelegateMixin { + @override + final GlobalKey navigatorKey; + + String _currentPath = AppRoutes.home; + String? _matchId; + String? _bookmakerId; + + AppRouterDelegate() : navigatorKey = GlobalKey(); + + String get currentPath => _currentPath; + + @override + RouteInformation? get currentConfiguration { + if (_matchId != null) { + return RouteInformation( + uri: Uri.parse('/match/$_matchId'), + ); + } + if (_bookmakerId != null) { + return RouteInformation( + uri: Uri.parse('/bookmaker/$_bookmakerId'), + ); + } + return RouteInformation(uri: Uri.parse(_currentPath)); + } + + @override + Future setNewRoutePath(RouteInformation configuration) async { + final uri = configuration.uri; + + if (uri.pathSegments.isEmpty) { + _currentPath = AppRoutes.home; + _matchId = null; + _bookmakerId = null; + } else if (uri.pathSegments.length == 2) { + if (uri.pathSegments[0] == 'match') { + _currentPath = AppRoutes.matchDetails; + _matchId = uri.pathSegments[1]; + } else if (uri.pathSegments[0] == 'bookmaker') { + _currentPath = AppRoutes.bookmakerDetails; + _bookmakerId = uri.pathSegments[1]; + } + } else if (uri.pathSegments.length == 1) { + if (uri.pathSegments[0] == 'settings') { + _currentPath = AppRoutes.settings; + } + } + + notifyListeners(); + } + + @override + Widget build(BuildContext context) { + return Navigator( + key: navigatorKey, + pages: [ + const MaterialPage( + key: ValueKey('HomePage'), + child: Placeholder(), // Replace with actual HomePage + ), + if (_matchId != null) + MaterialPage( + key: ValueKey('MatchDetailsPage-$_matchId'), + child: Placeholder(), // Replace with MatchDetailsPage + ), + if (_bookmakerId != null) + MaterialPage( + key: ValueKey('BookmakerDetailsPage-$_bookmakerId'), + child: Placeholder(), // Replace with BookmakerDetailsPage + ), + if (_currentPath == AppRoutes.settings) + const MaterialPage( + key: ValueKey('SettingsPage'), + child: Placeholder(), // Replace with SettingsPage + ), + ], + onPopPage: (route, result) { + if (!route.didPop(result)) { + return false; + } + + // Handle navigation state after pop + if (_matchId != null) { + _matchId = null; + notifyListeners(); + } else if (_bookmakerId != null) { + _bookmakerId = null; + notifyListeners(); + } else if (_currentPath == AppRoutes.settings) { + _currentPath = AppRoutes.home; + notifyListeners(); + } + + return true; + }, + ); + } + + /// Navigate to match details + void navigateToMatchDetails(String matchId) { + _matchId = matchId; + _currentPath = AppRoutes.matchDetails; + notifyListeners(); + } + + /// Navigate to bookmaker details + void navigateToBookmakerDetails(String bookmakerId) { + _bookmakerId = bookmakerId; + _currentPath = AppRoutes.bookmakerDetails; + notifyListeners(); + } + + /// Navigate to settings + void navigateToSettings() { + _currentPath = AppRoutes.settings; + notifyListeners(); + } + + /// Navigate to home + void navigateToHome() { + _currentPath = AppRoutes.home; + _matchId = null; + _bookmakerId = null; + notifyListeners(); + } +} + +/// Route information parser for Navigator 2.0 +class AppRouteInformationParser extends RouteInformationParser { + @override + Future parseRouteInformation( + RouteInformation routeInformation, + ) async { + return routeInformation; + } + + @override + RouteInformation? restoreRouteInformation(RouteInformation configuration) { + return configuration; + } +} diff --git a/frontend-flutter/pubspec.lock b/frontend-flutter/pubspec.lock new file mode 100644 index 0000000..dbb7c92 --- /dev/null +++ b/frontend-flutter/pubspec.lock @@ -0,0 +1,586 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d + url: "https://pub.dev" + source: hosted + version: "91.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08 + url: "https://pub.dev" + source: hosted + version: "8.4.1" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + bloc: + dependency: "direct main" + description: + name: bloc + sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" + url: "https://pub.dev" + source: hosted + version: "8.1.4" + bloc_test: + dependency: "direct dev" + description: + name: bloc_test + sha256: "165a6ec950d9252ebe36dc5335f2e6eb13055f33d56db0eeb7642768849b43d2" + url: "https://pub.dev" + source: hosted + version: "9.1.7" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" + url: "https://pub.dev" + source: hosted + version: "1.15.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + diff_match_patch: + dependency: transitive + description: + name: diff_match_patch + sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4" + url: "https://pub.dev" + source: hosted + version: "0.4.1" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" + url: "https://pub.dev" + source: hosted + version: "2.0.8" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a + url: "https://pub.dev" + source: hosted + version: "8.1.6" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3 + url: "https://pub.dev" + source: hosted + version: "14.8.1" + http: + dependency: "direct main" + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + mocktail: + dependency: "direct dev" + description: + name: mocktail + sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + provider: + dependency: transitive + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev" + source: hosted + version: "0.10.13" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test: + dependency: transitive + description: + name: test + sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" + url: "https://pub.dev" + source: hosted + version: "1.26.3" + test_api: + dependency: transitive + description: + name: test_api + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + url: "https://pub.dev" + source: hosted + version: "0.7.7" + test_core: + dependency: transitive + description: + name: test_core + sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" + url: "https://pub.dev" + source: hosted + version: "0.6.12" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.22.0" diff --git a/frontend-flutter/pubspec.yaml b/frontend-flutter/pubspec.yaml new file mode 100644 index 0000000..1ed78da --- /dev/null +++ b/frontend-flutter/pubspec.yaml @@ -0,0 +1,44 @@ +name: scomettiapp +description: A Flutter betting odds comparison application +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: '>=3.5.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + + # State Management + flutter_bloc: ^8.1.6 + bloc: ^8.1.4 + equatable: ^2.0.5 + + # HTTP Client + http: ^1.2.2 + + # Material Design + cupertino_icons: ^1.0.8 + + # Utility + intl: ^0.19.0 # Per formattazione date + go_router: ^14.6.2 # Per Navigator 2.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^4.0.0 + bloc_test: ^9.1.7 + mocktail: ^1.0.4 + +flutter: + uses-material-design: true + + # assets: + # - images/ + + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf