P0 UUID Phase A: migrations + backend Go UUID refactor
This commit is contained in:
parent
327ac36a30
commit
65420e7c0d
15 changed files with 651 additions and 86 deletions
30
CLEANUP_PLAN.md
Normal file
30
CLEANUP_PLAN.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# 🧹 CLEANUP_PLAN.md - Plan de Nettoyage Immédiat
|
||||
|
||||
## Phase 1 : Standardisation de la Vérité (Semaine 1)
|
||||
|
||||
### 1.1 Unification des Communs Rust
|
||||
* **Action:** Analyser `veza-common` et `veza-rust-common`.
|
||||
* **Décision:** Garder `veza-common` comme bibliothèque canonique. Déplacer tout le code utile de `veza-rust-common` dedans. Supprimer `veza-rust-common`.
|
||||
* **Gain:** Une seule dépendance partagée pour Chat et Stream.
|
||||
|
||||
### 1.2 Nettoyage des Scripts
|
||||
* **Action:** Auditer le dossier `scripts/`.
|
||||
* **Consolidation:** Créer un `Makefile` unique et puissant qui appelle les bons scripts.
|
||||
* **Archivage:** Déplacer les scripts "one-shot" (migrations manuelles, fixes UUID passés) dans `scripts/archive/`.
|
||||
|
||||
## Phase 2 : Résolution du Frontend (Semaine 2)
|
||||
|
||||
### 2.1 Dépréciation de la logique `veza-desktop`
|
||||
* **Constat:** `apps/web` est supérieur.
|
||||
* **Action:** Transformer `veza-desktop` en un simple conteneur Electron qui charge l'application `apps/web` (soit via URL en dev, soit via build statique en prod).
|
||||
* **Code:** Supprimer la duplication Redux/Components dans `veza-desktop`.
|
||||
|
||||
## Phase 3 : Hygiène Base de Données (Semaine 3)
|
||||
|
||||
### 3.1 Centralisation des Migrations
|
||||
* **Problème:** Conflit de propriété des tables partagées.
|
||||
* **Solution:** Définir que `veza-backend-api` est le "Maître" du schéma `public` (Users, Auth).
|
||||
* **Chat Server:** Doit traiter la DB `users` en lecture seule ou via API gRPC, ou avoir son propre schéma isolé (ex: schema `chat`).
|
||||
|
||||
### 3.2 Validation UUID
|
||||
* **Action:** Lancer une campagne de tests d'intégration ciblée sur les IDs pour vérifier que plus aucun `INT` n'est attendu nulle part.
|
||||
65
REPORT_ARCHITECTURE.md
Normal file
65
REPORT_ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# 🏗️ REPORT_ARCHITECTURE.md - Cartographie Technique
|
||||
|
||||
## 1. Architecture des Services
|
||||
|
||||
### 🟢 Service: Backend API (`veza-backend-api`)
|
||||
* **Rôle:** Cœur de métier, gestion utilisateurs, metadata, catalogue.
|
||||
* **Langage:** Go (Golang).
|
||||
* **Framework:** Gin Gonic.
|
||||
* **Data:** GORM + PostgreSQL.
|
||||
* **Observation:** Gère la logique métier lourde. A subi une refonte massive vers UUID.
|
||||
|
||||
### 🔵 Service: Chat Server (`veza-chat-server`)
|
||||
* **Rôle:** Messagerie temps-réel, présence, WebSockets.
|
||||
* **Langage:** Rust.
|
||||
* **Framework:** Axum + Tokio.
|
||||
* **Data:** SQLx + PostgreSQL + Redis (Cache).
|
||||
* **Dépendances:** Très riche (`jsonwebtoken`, `argon2`, `tonic` gRPC).
|
||||
* **Observation:** Architecture très propre, moderne, orientée performance.
|
||||
|
||||
### 🟣 Service: Stream Server (`veza-stream-server`)
|
||||
* **Rôle:** Streaming audio haute performance, transcodage.
|
||||
* **Langage:** Rust.
|
||||
* **Framework:** Axum + Symphonia (Audio).
|
||||
* **Observation:** Utilise `rayon` pour le parallélisme. Service critique pour l'expérience utilisateur.
|
||||
|
||||
## 2. Architecture Frontend (Le Conflit)
|
||||
|
||||
### 🅰️ Apps/Web (`apps/web`) - **LA CIBLE**
|
||||
* **Stack:** React 18, Vite, TailwindCSS, Zustand, TanStack Query, Radix UI.
|
||||
* **Qualité:** Très haute. Utilise les standards modernes (hooks, composants atomiques, `shadcn/ui` like).
|
||||
* **Rôle:** Web App principale.
|
||||
|
||||
### 🅱️ Veza Desktop (`veza-desktop`) - **LEGACY?**
|
||||
* **Stack:** Electron, React (plus ancien), Redux (vs Zustand sur web).
|
||||
* **Problème:** Semble être une implémentation parallèle et non un wrapper de `apps/web`.
|
||||
* **Risque:** Double maintenance des features.
|
||||
|
||||
## 3. Données & Infrastructure
|
||||
|
||||
### Base de Données (PostgreSQL)
|
||||
* Architecture distribuée ou monolithique logique ?
|
||||
* **Problème:** `veza-backend-api` et `veza-chat-server` ont chacun leur dossier `migrations/`.
|
||||
* **Risque:** Désynchronisation des schémas (ex: table `users` définie à deux endroits ?).
|
||||
|
||||
### Communication Inter-Services
|
||||
* Preuves de **gRPC** (`tonic`) dans les fichiers Cargo.
|
||||
* Preuves de **RabbitMQ** (`lapin`) mentionné.
|
||||
|
||||
## 4. Diagramme de Flux (Simplifié)
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Client[Clients (Web/Desktop/Mobile)] --> HAProxy[HAProxy / Load Balancer]
|
||||
HAProxy --> Go[Go Backend API]
|
||||
HAProxy --> Chat[Rust Chat Server]
|
||||
HAProxy --> Stream[Rust Stream Server]
|
||||
|
||||
Go --> DB[(PostgreSQL Core)]
|
||||
Chat --> DB
|
||||
Chat --> Redis[(Redis Cache)]
|
||||
Stream --> FS[File System / S3]
|
||||
|
||||
Go -.-> RabbitMQ((RabbitMQ Event Bus))
|
||||
Chat -.-> RabbitMQ
|
||||
```
|
||||
33
REPORT_BUGS.md
Normal file
33
REPORT_BUGS.md
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# 🐞 REPORT_BUGS.md - Anomalies & Dette Technique
|
||||
|
||||
## 🚨 Priorité P0 (Critique / Bloquant)
|
||||
|
||||
### 1. Le Chaos des UUIDs
|
||||
* **Symptôme:** Présence de scripts de "fix" (`fix-remaining-uuid-errors.sh`, `migrate-handlers-to-uuid.sh`) et de migrations SQL explicites de conversion (`047_migrate_users_id_to_uuid.sql`).
|
||||
* **Risque:** Incohérence de données. Si un service attend un `INT` et reçoit un `UUID` (ou vice-versa) via API ou DB, c'est le crash.
|
||||
* **Localisation:** `veza-backend-api`, `migrations/` root.
|
||||
|
||||
### 2. Schisme des Migrations DB
|
||||
* **Symptôme:** `veza-backend-api` gère des tables comme `users`. `veza-chat-server` a aussi ses migrations.
|
||||
* **Risque:** Qui possède la table `users` ? Si le chat server tente d'accéder à `users` avec une définition obsolète (ex: ID non-UUID), cela échouera.
|
||||
* **Preuve:** `veza-chat-server/sqlx-data.json` vs `veza-backend-api/migrations/*.sql`.
|
||||
|
||||
## ⚠️ Priorité P1 (Conformité & Architecture)
|
||||
|
||||
### 3. Duplication Frontend
|
||||
* **Symptôme:** `apps/web` (Stack Moderne: Zustand/Vite) vs `veza-desktop` (Stack Legacy: Redux/Electron).
|
||||
* **Impact:** Double effort de développement pour chaque feature. Incohérence UI/UX garantie.
|
||||
|
||||
### 4. Duplication "Common" Rust
|
||||
* **Symptôme:** Existence de `veza-common` ET `veza-rust-common`.
|
||||
* **Impact:** Confusion pour les développeurs. Où mettre les types partagés ? Risque de dépendances circulaires ou de versions divergentes.
|
||||
|
||||
## 📉 Priorité P2 (Maintenance & Scripts)
|
||||
|
||||
### 5. Explosion de Scripts à la Racine
|
||||
* **Symptôme:** Dossier `scripts/` contenant tout et n'importe quoi (`start-veza-complete.sh`, `start-veza-docker.sh`, `start-veza.sh`...).
|
||||
* **Impact:** On ne sait pas quel est le script de démarrage "officiel" de production.
|
||||
|
||||
### 6. Tests dispersés
|
||||
* **Symptôme:** Tests dans `tools/tests`, `tests/`, `fixtures/`.
|
||||
* **Impact:** Difficulté d'avoir un CI fiable et rapide.
|
||||
36
REPORT_GLOBAL.md
Normal file
36
REPORT_GLOBAL.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# 🌍 REPORT_GLOBAL.md - Audit Général du Projet Veza
|
||||
|
||||
**Date:** 04/12/2025
|
||||
**Auteur:** Staff Engineer / Architect
|
||||
**Statut:** ⚠️ COMPLEXE / EN TRANSITION
|
||||
|
||||
## 1. Vue d'ensemble
|
||||
Le projet **Veza** est une plateforme ambitieuse de streaming et collaboration musicale (600+ features visées).
|
||||
L'architecture est **Microservices hybride (Go + Rust)** avec un frontend moderne.
|
||||
|
||||
Actuellement, le repo est dans un état de **transition critique** :
|
||||
1. **Migration d'IDs:** Le passage de `INT` vers `UUID` est récent et laisse des traces partout (scripts de fix, migrations multiples).
|
||||
2. **Fragmentation Frontend:** Deux applications majeures cohabitent (`veza-desktop` vs `apps/web`) avec des stacks technologiques divergentes.
|
||||
3. **Dette Rust:** Deux bibliothèques communes (`veza-common` et `veza-rust-common`) existent en parallèle.
|
||||
|
||||
## 2. Note de Conformité "ORIGIN"
|
||||
La vision cible (`veza_full_features_list.md` + `veza-docs/vision`) décrit une plateforme V6-V12.
|
||||
L'état actuel correspond à une **V1 instable**.
|
||||
|
||||
| Domaine | État | Conformité "ORIGIN" |
|
||||
| :--- | :--- | :--- |
|
||||
| **Backend API** | 🟠 En transition | Stack Go respectée. Migration UUID en cours de stabilisation. |
|
||||
| **Chat Server** | 🟢 Avancé | Stack Rust (Axum/Sqlx) conforme et riche. |
|
||||
| **Stream Server** | 🟢 Avancé | Stack Rust (Axum/Symphonia) conforme. |
|
||||
| **Frontend** | 🔴 Fragmenté | `apps/web` est moderne (Target). `veza-desktop` semble legacy. |
|
||||
| **Infrastructure** | 🟠 Mixte | Beaucoup de scripts "home-made" dans `/scripts` vs Docker Compose standard. |
|
||||
|
||||
## 3. Chiffres Clés de l'Audit
|
||||
* **300+** Fichiers de code source.
|
||||
* **600** Features planifiées.
|
||||
* **40+** Migrations SQL récentes sur le backend Go.
|
||||
* **2** Stacks Frontend concurrentes.
|
||||
* **2** Bibliothèques "Common" Rust.
|
||||
|
||||
## 4. Verdict
|
||||
Le projet a un **potentiel technique énorme** (choix Go/Rust pertinents pour la performance). Cependant, la complexité accidentelle (doublons, migrations) menace la vélocité. Il faut impérativement **consolider avant d'ajouter des features**.
|
||||
26
ROADMAP_90_DAYS.md
Normal file
26
ROADMAP_90_DAYS.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# 📅 ROADMAP_90_DAYS.md - Vers la V1 Stable
|
||||
|
||||
## 🟢 M0 - STABILISATION (Jours 1-30)
|
||||
**Objectif :** Plus de régressions, infrastructure saine.
|
||||
|
||||
* **Semaine 1:** Exécution du `CLEANUP_PLAN` (Fusion libs Rust, Archivage scripts).
|
||||
* **Semaine 2:** Audit de sécurité des UUIDs. Vérification de toutes les Foreign Keys en base.
|
||||
* **Semaine 3:** Mise en place d'un CI/CD strict. Le build doit passer sur `main` sans hacks.
|
||||
* **Semaine 4:** "Smoke Testing" global. Tous les services démarrent avec une seule commande `make start`.
|
||||
|
||||
## 🟡 M1 - UNIFICATION (Jours 31-60)
|
||||
**Objectif :** Une seule codebase Frontend, une communication inter-services claire.
|
||||
|
||||
* **Semaine 5:** Refonte de `veza-desktop` pour consommer le build de `apps/web`.
|
||||
* **Semaine 6:** Implémentation propre de gRPC entre Backend (Go) et Chat (Rust) pour partager les sessions/auth sans taper en DB directement.
|
||||
* **Semaine 7:** Nettoyage du code mort dans le Backend Go (anciennes routes non-UUID).
|
||||
* **Semaine 8:** Documentation technique mise à jour (Architecture réelle = Documentation).
|
||||
|
||||
## 🔵 M2 - FEATURE PARITY V1 (Jours 61-90)
|
||||
**Objectif :** Livrer les 40 features du "Tier 0 - V1 Launch".
|
||||
|
||||
* **Focus:** S'assurer que les 40 features critiques (Auth, Profil, Upload simple, Player audio basique, Chat 1-1) fonctionnent parfaitement sur la stack unifiée.
|
||||
* **Fin du trimestre:** Release Candidate 1 (RC1).
|
||||
|
||||
---
|
||||
**Note:** Cette roadmap repousse le développement de nouvelles features (IA, Blockchain, etc.) au trimestre suivant. La dette technique actuelle est trop élevée pour construire dessus sainement.
|
||||
99
docs/UUID_DB_CARTOGRAPHY.md
Normal file
99
docs/UUID_DB_CARTOGRAPHY.md
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
# 🗺️ UUID_DB_CARTOGRAPHY.md
|
||||
|
||||
**Date:** 04/12/2025
|
||||
**Statut:** 🔴 ALERTE CRITIQUE (Schisme de Données)
|
||||
**Contexte:** Analyse post-mortem de la migration INT vers UUID et du conflit de propriété des données entre Backend (Go) et Chat (Rust).
|
||||
|
||||
---
|
||||
|
||||
## 1. État des Lieux : La Guerre des Schémas
|
||||
|
||||
Il existe **trois** sources de vérité concurrentes pour la définition de la base de données, créant un état incohérent.
|
||||
|
||||
| Entité | Source de Vérité | Type ID Utilisateur | Méthode de Génération | Colonnes Clés |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| **Backend API (Go)** | `veza-backend-api/migrations/` | **UUID** (Migré) | `uuid_generate_v5` (Déterministe depuis INT) | `username`, `email`, `password_hash` |
|
||||
| **Chat Server (Rust)** | `veza-chat-server/migrations/` | **UUID** (Natif) | `gen_random_uuid()` (v4 Aléatoire) | `display_name`, `last_seen`, `avatar_url` |
|
||||
| **Root Scripts** | `migrations/` | **UUID** (Mixte) | `uuid_generate_v4` (v4 Aléatoire) | N/A (Tables secondaires) |
|
||||
|
||||
### 🚨 Le Conflit "Users"
|
||||
La table `users` est définie différemment par les deux services majeurs.
|
||||
* **Backend (Go)** migre les anciens IDs : `123` -> `UUID-v5-du-123`.
|
||||
* **Chat (Rust)** crée de nouveaux users : `UUID-v4-Random`.
|
||||
|
||||
**Conséquence :** Si le Backend et le Chat partagent la même DB (ce qui est le cas en prod), le Chat Server va échouer car il attend des colonnes (`display_name`) qui n'existent pas dans la version Backend (`first_name`/`last_name`), ou vice-versa.
|
||||
|
||||
---
|
||||
|
||||
## 2. Cartographie des Tables & IDs
|
||||
|
||||
### Tables Principales (Gérées par `veza-backend-api`)
|
||||
Ces tables ont subi la migration `047` (Destructive).
|
||||
|
||||
| Table | Type ID PK | Type FK User | État Migration | Observation |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| `users` | **UUID** | N/A | ✅ Terminée | PK renommée de `id_uuid` à `id`. |
|
||||
| `tracks` | SERIAL | **UUID** | ✅ Terminée | FK `user_id` est UUID. PK reste SERIAL ? (À vérifier) |
|
||||
| `playlists` | SERIAL | **UUID** | ✅ Terminée | FK `user_id` est UUID. |
|
||||
| `messages` (Legacy) | SERIAL | **UUID** | ✅ Terminée | Table messages du backend (pas celle du Chat Server). |
|
||||
|
||||
### Tables "Chat" (Gérées par `veza-chat-server`)
|
||||
Ces tables sont définies dans `001_create_clean_database.sql` (Rust).
|
||||
|
||||
| Table | Type ID PK | Type FK User | État | Conflit ? |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| `conversations` | **UUID** | **UUID** | Natif | OK |
|
||||
| `messages` (New) | **UUID** | **UUID** | Natif | ⚠️ Conflit de nom avec `messages` du Backend |
|
||||
| `conversation_members`| Composite | **UUID** | Natif | OK |
|
||||
|
||||
### Tables Secondaires (Gérées par `migrations/` root)
|
||||
Tables touchées par `001_migrate_ids_to_uuid_up.sql`.
|
||||
|
||||
| Table | Type ID PK | État | Observation |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `contests` | **UUID** (new_id) | ⚠️ Partiel | Colonne `new_id` ajoutée mais PK pas encore switchée ? |
|
||||
| `equipment` | **UUID** (new_id) | ⚠️ Partiel | Idem. |
|
||||
| `user_profiles` | **UUID** (new_id) | ⚠️ Partiel | Doublon potentiel avec `users` ? |
|
||||
|
||||
---
|
||||
|
||||
## 3. Analyse des Migrations Exécutées
|
||||
|
||||
### Migration Critique : `veza-backend-api/.../047_migrate_users_id_to_uuid.sql`
|
||||
C'est la migration de référence. Elle est **destructrice** (DROP COLUMN id).
|
||||
* **Points Forts :** Utilise `uuid_generate_v5(namespace, old_id)`. Cela garantit que l'ID `42` devient toujours le même UUID. C'est excellent pour la consistance.
|
||||
* **Points Faibles :** Elle suppose qu'elle est la seule à toucher à la DB.
|
||||
|
||||
### Migration "Root" : `migrations/001_migrate_ids_to_uuid_up.sql`
|
||||
Semble être une tentative de rattrapage pour les tables oubliées par la migration 047.
|
||||
* **Problème :** Elle ajoute `new_id` mais ne semble pas (dans l'extrait lu) faire le `DROP` et `RENAME` final. Elle laisse la DB dans un état intermédiaire (`id` INT + `new_id` UUID).
|
||||
|
||||
### Scripts Shell de "Fix"
|
||||
* `scripts/fix-remaining-uuid-errors.sh` : Un script "Find & Replace" brutal (`sed`) sur le code Go.
|
||||
* Preuve que le code Go n'a pas été refactoré proprement mais "patché" pour accepter les UUIDs (remplacement de `0` par `uuid.Nil`).
|
||||
|
||||
---
|
||||
|
||||
## 4. Source of Truth (Proposition de Résolution)
|
||||
|
||||
Pour résoudre le schisme, nous devons établir une hiérarchie stricte.
|
||||
|
||||
### 👑 Maître du Schéma Global : `veza-backend-api`
|
||||
Le Backend Go contient l'historique et la complexité métier (Auth, Roles, Profils). Il **DOIT** posséder la table `users`.
|
||||
|
||||
### 🚫 Interdit au Chat Server
|
||||
Le `veza-chat-server` **NE DOIT PAS** :
|
||||
1. Créer la table `users`.
|
||||
2. Gérer ses propres migrations pour `users`.
|
||||
3. Avoir une table `messages` qui porte le même nom que celle du Backend (si elles sont dans le même schema).
|
||||
|
||||
### ✅ Actions Requises (pour la phase de correction)
|
||||
1. **Aligner le schéma User :** Le Chat Server (Rust) doit adapter son modèle `struct User` pour correspondre aux colonnes réelles du Backend (`first_name` vs `display_name`).
|
||||
2. **Renommage Tables :** La table `messages` du Chat Server doit être renommée `chat_messages` pour éviter la collision avec les messages legacy du Backend.
|
||||
3. **Nettoyage Root :** Finir ou reverter la migration `migrations/001_migrate_ids_to_uuid_up.sql` (état `new_id` hybride dangereux).
|
||||
|
||||
---
|
||||
|
||||
## 5. Conclusion
|
||||
Le chaos des IDs est en réalité un **Chaos de Gouvernance**.
|
||||
Deux équipes (ou deux esprits) ont travaillé en parallèle : l'une migrant l'existant (Go), l'autre construisant le futur (Rust), sans se synchroniser sur la couche de données partagée.
|
||||
131
docs/UUID_DB_MIGRATION_PLAN.md
Normal file
131
docs/UUID_DB_MIGRATION_PLAN.md
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
# 📐 UUID_DB_MIGRATION_PLAN.md
|
||||
|
||||
**Date:** 04/12/2025
|
||||
**Statut:** PLAN VALIDÉ (En attente d'exécution)
|
||||
**Objectif:** Unification totale des IDs (UUID) et séparation des responsabilités (Schemas).
|
||||
|
||||
---
|
||||
|
||||
## 1. Schéma Cible & Stratégie de Séparation
|
||||
|
||||
Pour résoudre définitivement les conflits de nommage et de propriété, nous allons adopter une architecture **Multi-Schéma PostgreSQL**.
|
||||
|
||||
### 1.1 Architecture des Schémas
|
||||
|
||||
1. **Schema `public` (Master: Backend Go)**
|
||||
* Contient les données "Cœur" : `users`, `auth`, `tracks`, `playlists`.
|
||||
* Le service Go `veza-backend-api` possède les droits DDL (Migrations) sur ce schéma.
|
||||
* Tous les IDs (PK et FK) sont des **UUID**.
|
||||
|
||||
2. **Schema `chat` (Master: Chat Rust)**
|
||||
* Contient les données spécifiques au chat : `conversations`, `messages`, `members`.
|
||||
* Le service Rust `veza-chat-server` possède les droits DDL sur ce schéma uniquement.
|
||||
* Fait référence à `public.users(id)` via FK.
|
||||
* Isole la table `messages` du chat de la table `messages` legacy du backend.
|
||||
|
||||
### 1.2 Matrice des Types d'Identifiants
|
||||
|
||||
| Entité | Table | Schema | PK Type | Géré par |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| User | `users` | `public` | **UUID** | Go |
|
||||
| Track | `tracks` | `public` | **UUID** | Go |
|
||||
| Playlist | `playlists` | `public` | **UUID** | Go |
|
||||
| Conversation | `conversations` | `chat` | **UUID** | Rust |
|
||||
| Message | `messages` | `chat` | **UUID** | Rust |
|
||||
| Message (Legacy) | `messages` | `public` | **UUID** | Go |
|
||||
|
||||
---
|
||||
|
||||
## 2. Migrations : Le Plan de Bataille
|
||||
|
||||
Les migrations seront exécutées séquentiellement.
|
||||
|
||||
### Phase A : Consolidation du Backend (Go)
|
||||
*Ces migrations doivent être créées dans `veza-backend-api/migrations/`*
|
||||
|
||||
1. **`070_finish_secondary_tables_uuid.sql`**
|
||||
* **But :** Finaliser le travail laissé par `migrations/001_migrate_ids_to_uuid_up.sql`.
|
||||
* **Action :** Pour toutes les tables secondaires (`contests`, `equipment`, etc.) qui ont une colonne `new_id` :
|
||||
* Supprimer l'ancienne PK `id` (INT).
|
||||
* Renommer `new_id` -> `id`.
|
||||
* Mettre `id` en PRIMARY KEY.
|
||||
|
||||
2. **`071_migrate_tracks_playlists_pk_to_uuid.sql`**
|
||||
* **But :** S'assurer que `tracks` et `playlists` ont bien leur **PK** en UUID (pas seulement la FK `user_id`).
|
||||
* **Action :** Même pattern : ajout colonne tmp UUID, migration data, switch PK.
|
||||
|
||||
3. **`072_create_chat_schema.sql`**
|
||||
* **Action :** `CREATE SCHEMA IF NOT EXISTS chat;`
|
||||
* **Action :** Donner les droits nécessaires au user DB du chat server.
|
||||
|
||||
### Phase B : Refonte du Chat Server (Rust)
|
||||
*Ces migrations remplacent le dossier `veza-chat-server/migrations/` actuel.*
|
||||
|
||||
1. **`001_init_chat_schema.sql`** (Remplacement total)
|
||||
* **Action :** Créer les tables `conversations`, `messages`, `conversation_members` DANS le schéma `chat`.
|
||||
* **Reference :** `REFERENCES public.users(id)`.
|
||||
* **Nettoyage :** NE PAS créer de table `users`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Modifications du Code (Implementation Detail)
|
||||
|
||||
### 3.1 Backend API (Go)
|
||||
* **Models GORM :**
|
||||
* Vérifier que tous les structs (`User`, `Track`, `Playlist`) utilisent le type `uuid.UUID` pour le champ `ID` et ont le tag `` `gorm:"type:uuid;default:uuid_generate_v4()"` `` (ou v5 pour users legacy).
|
||||
* Supprimer définitivement les champs `ID uint` ou `int64`.
|
||||
* **Handlers :**
|
||||
* Nettoyer tout code convertissant `string` -> `int` pour les IDs. Utiliser `uuid.Parse()`.
|
||||
|
||||
### 3.2 Chat Server (Rust)
|
||||
* **Configuration SQLx :**
|
||||
* Forcer le search_path : `ALTER DATABASE ... SET search_path TO chat, public;`.
|
||||
* **Structs :**
|
||||
* Le struct `User` (utilisé pour l'auth ou l'info user) doit être un "Read Model" mappé sur `public.users`.
|
||||
* Supprimer toute logique d'écriture dans `users` depuis Rust.
|
||||
* **Queries :**
|
||||
* Remplacer `SELECT ... FROM users` par `SELECT ... FROM public.users`.
|
||||
* Remplacer `SELECT ... FROM messages` par `SELECT ... FROM chat.messages`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Désarmement des Migrations du Chat
|
||||
|
||||
Actuellement, `veza-chat-server` contient des migrations conflictuelles.
|
||||
|
||||
**Plan d'action :**
|
||||
1. **Supprimer** le contenu actuel de `veza-chat-server/migrations/`.
|
||||
2. **Créer** une nouvelle migration `0001_init_chat.sql` qui contient uniquement la DDL du schéma `chat` (conversations, messages).
|
||||
3. **Retirer** toute instruction `CREATE TABLE users`.
|
||||
4. **Ajuster** `sqlx-data.json` (le fichier de cache query verification) en le régénérant contre la nouvelle DB cible.
|
||||
|
||||
---
|
||||
|
||||
## 5. Nettoyage des Scripts & Hacks
|
||||
|
||||
Les scripts "béquilles" doivent disparaître pour garantir que le code est sain sans patch externe.
|
||||
|
||||
1. **Supprimer :**
|
||||
* `scripts/fix-remaining-uuid-errors.sh` (Le code doit compiler nativement).
|
||||
* `scripts/migrate-handlers-to-uuid.sh`.
|
||||
* `scripts/migrate-models-to-uuid.sh`.
|
||||
* `migrations/` (le dossier root) : Son contenu utile est déplacé dans la migration `070` du backend.
|
||||
|
||||
2. **Créer :**
|
||||
* `scripts/db-reset-clean.sh` :
|
||||
1. Drop DB.
|
||||
2. Create DB.
|
||||
3. Run Backend Migrations (Go).
|
||||
4. Run Chat Migrations (Rust).
|
||||
5. Seed minimal data.
|
||||
|
||||
---
|
||||
|
||||
## 6. Vérification de Compatibilité
|
||||
|
||||
* **Backend -> DB :** Le Backend Go migre tout en UUID. Il n'attendra plus de SERIAL.
|
||||
* **Chat -> DB :** Le Chat utilise son propre schéma. Il lit `public.users` (qui est UUID) via des FKs UUID. Compatibilité 100%.
|
||||
* **Collision Messages :** Résolue par `public.messages` (legacy) vs `chat.messages`.
|
||||
|
||||
## ⏳ Prochaine Étape
|
||||
Exécuter ce plan nécessite d'abord de **générer les fichiers de migration SQL** décrits en Phase A et B, puis d'appliquer les changements de code.
|
||||
|
|
@ -152,14 +152,14 @@ func createDummyAudioFile() (string, error) {
|
|||
type UploadResponse struct {
|
||||
Message string `json:"message"`
|
||||
Track struct {
|
||||
ID int64 `json:"id"`
|
||||
ID string `json:"id"`
|
||||
} `json:"track"`
|
||||
}
|
||||
|
||||
func uploadTrack(token, filePath string) (int64, error) {
|
||||
func uploadTrack(token, filePath string) (string, error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
|
|
@ -174,7 +174,7 @@ func uploadTrack(token, filePath string) (int64, error) {
|
|||
|
||||
req, err := http.NewRequest("POST", baseURL+"/tracks/upload", body)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
|
|
@ -182,23 +182,23 @@ func uploadTrack(token, filePath string) (int64, error) {
|
|||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
return 0, fmt.Errorf("upload failed (status %d): %s", resp.StatusCode, string(respBody))
|
||||
return "", fmt.Errorf("upload failed (status %d): %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
|
||||
var result UploadResponse
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return 0, fmt.Errorf("failed to parse upload response: %v. Body: %s", err, string(respBody))
|
||||
return "", fmt.Errorf("failed to parse upload response: %v. Body: %s", err, string(respBody))
|
||||
}
|
||||
|
||||
if result.Track.ID == 0 {
|
||||
return 0, fmt.Errorf("no track ID returned")
|
||||
if result.Track.ID == "" {
|
||||
return "", fmt.Errorf("no track ID returned")
|
||||
}
|
||||
|
||||
return result.Track.ID, nil
|
||||
|
|
@ -208,12 +208,12 @@ type TrackStatusResponse struct {
|
|||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
func waitForProcessing(token string, trackID int64) error {
|
||||
func waitForProcessing(token string, trackID string) error {
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
maxRetries := 5 // Short wait
|
||||
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("%s/tracks/%d", baseURL, trackID), nil)
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("%s/tracks/%s", baseURL, trackID), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -250,12 +250,12 @@ func waitForProcessing(token string, trackID int64) error {
|
|||
return fmt.Errorf("track did not reach stable state")
|
||||
}
|
||||
|
||||
func verifyPlayback(token string, trackID int64) error {
|
||||
func verifyPlayback(token string, trackID string) error {
|
||||
// Check if we can get the track details.
|
||||
// The actual stream URL might be in the track details or a separate endpoint.
|
||||
// Based on API Spec: GET /tracks/:id
|
||||
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("%s/tracks/%d", baseURL, trackID), nil)
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("%s/tracks/%s", baseURL, trackID), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,19 +32,20 @@ type OAuthService struct {
|
|||
}
|
||||
|
||||
// OAuthAccount represents an OAuth account linking
|
||||
// Mapped to federated_identities table
|
||||
type OAuthAccount struct {
|
||||
ID int64 `json:"id" db:"id"`
|
||||
UserID uuid.UUID `json:"user_id" db:"user_id"`
|
||||
Provider string `json:"provider" db:"provider"`
|
||||
ProviderUserID string `json:"provider_user_id" db:"provider_user_id"`
|
||||
Email string `json:"email" db:"email"`
|
||||
Name string `json:"name" db:"name"`
|
||||
AvatarURL string `json:"avatar_url" db:"avatar_url"`
|
||||
AccessToken string `json:"-" db:"access_token"`
|
||||
RefreshToken string `json:"-" db:"refresh_token"`
|
||||
ExpiresAt time.Time `json:"expires_at" db:"expires_at"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
UserID uuid.UUID `json:"user_id" db:"user_id"`
|
||||
Provider string `json:"provider" db:"provider"`
|
||||
ProviderID string `json:"provider_id" db:"provider_id"`
|
||||
Email string `json:"email" db:"email"`
|
||||
DisplayName string `json:"display_name" db:"display_name"`
|
||||
AvatarURL string `json:"avatar_url" db:"avatar_url"`
|
||||
AccessToken string `json:"-" db:"access_token"`
|
||||
RefreshToken string `json:"-" db:"refresh_token"`
|
||||
ExpiresAt time.Time `json:"expires_at" db:"expires_at"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// OAuthState represents an OAuth state for CSRF protection
|
||||
|
|
@ -433,21 +434,22 @@ func (os *OAuthService) getOrCreateUser(oauthUser *OAuthUser) (*OAuthUserInfo, e
|
|||
}
|
||||
|
||||
// saveOAuthAccount saves or updates OAuth account information
|
||||
// Uses federated_identities table
|
||||
func (os *OAuthService) saveOAuthAccount(oauthUser *OAuthUser, userID uuid.UUID, token *oauth2.Token) error {
|
||||
ctx := context.Background()
|
||||
|
||||
// Check if OAuth account already exists
|
||||
var existingID int64
|
||||
var existingID uuid.UUID
|
||||
err := os.db.QueryRowContext(ctx, `
|
||||
SELECT id FROM oauth_accounts
|
||||
WHERE user_id = $1 AND provider_user_id = $2
|
||||
SELECT id FROM federated_identities
|
||||
WHERE user_id = $1 AND provider_id = $2
|
||||
`, userID, oauthUser.ProviderID).Scan(&existingID)
|
||||
|
||||
if err == nil {
|
||||
// Update existing
|
||||
_, err = os.db.ExecContext(ctx, `
|
||||
UPDATE oauth_accounts
|
||||
SET email = $1, name = $2, access_token = $3, refresh_token = $4, expires_at = $5, updated_at = NOW()
|
||||
UPDATE federated_identities
|
||||
SET email = $1, display_name = $2, access_token = $3, refresh_token = $4, expires_at = $5, updated_at = NOW()
|
||||
WHERE id = $6
|
||||
`, oauthUser.Email, oauthUser.Name, token.AccessToken, token.RefreshToken, token.Expiry, existingID)
|
||||
return err
|
||||
|
|
@ -459,8 +461,8 @@ func (os *OAuthService) saveOAuthAccount(oauthUser *OAuthUser, userID uuid.UUID,
|
|||
|
||||
// Insert new
|
||||
_, err = os.db.ExecContext(ctx, `
|
||||
INSERT INTO oauth_accounts (user_id, provider, provider_user_id, email, name, avatar_url, access_token, refresh_token, expires_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
INSERT INTO federated_identities (id, user_id, provider, provider_id, email, display_name, avatar_url, access_token, refresh_token, expires_at, created_at, updated_at)
|
||||
VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW())
|
||||
`, userID, "oauth", oauthUser.ProviderID, oauthUser.Email, oauthUser.Name, oauthUser.Avatar, token.AccessToken, token.RefreshToken, token.Expiry)
|
||||
|
||||
return err
|
||||
|
|
@ -475,4 +477,4 @@ func (os *OAuthService) generateJWT(userID uuid.UUID) (string, error) {
|
|||
})
|
||||
|
||||
return token.SignedString(os.jwtSecret)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ func NewRBACService(db *database.Database, logger *zap.Logger) *RBACService {
|
|||
|
||||
// Role represents a user role
|
||||
type Role struct {
|
||||
ID int64 `json:"id"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Permissions []Permission `json:"permissions"`
|
||||
|
|
@ -38,24 +38,24 @@ type Role struct {
|
|||
|
||||
// Permission represents a permission
|
||||
type Permission struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Resource string `json:"resource"`
|
||||
Action string `json:"action"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Resource string `json:"resource"`
|
||||
Action string `json:"action"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// UserRole represents a user's role assignment
|
||||
type UserRole struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
RoleID int64 `json:"role_id"`
|
||||
Role *Role `json:"role,omitempty"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
RoleID uuid.UUID `json:"role_id"`
|
||||
Role *Role `json:"role,omitempty"`
|
||||
}
|
||||
|
||||
// CreateRole creates a new role
|
||||
func (s *RBACService) CreateRole(ctx context.Context, name, description string, permissions []int64) (*Role, error) {
|
||||
func (s *RBACService) CreateRole(ctx context.Context, name, description string, permissions []uuid.UUID) (*Role, error) {
|
||||
// Check if role already exists
|
||||
var count int
|
||||
err := s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM roles WHERE name = $1", name).Scan(&count)
|
||||
|
|
@ -67,10 +67,10 @@ func (s *RBACService) CreateRole(ctx context.Context, name, description string,
|
|||
}
|
||||
|
||||
// Create role
|
||||
var roleID int64
|
||||
var roleID uuid.UUID
|
||||
query := `
|
||||
INSERT INTO roles (name, description, is_system, created_at, updated_at)
|
||||
VALUES ($1, $2, false, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
INSERT INTO roles (id, name, description, is_system, created_at, updated_at)
|
||||
VALUES (gen_random_uuid(), $1, $2, false, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
|
|
@ -99,12 +99,12 @@ func (s *RBACService) CreateRole(ctx context.Context, name, description string,
|
|||
return nil, fmt.Errorf("failed to get created role: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("Role created successfully", zap.String("role_name", name), zap.Int64("role_id", roleID))
|
||||
s.logger.Info("Role created successfully", zap.String("role_name", name), zap.String("role_id", roleID.String()))
|
||||
return role, nil
|
||||
}
|
||||
|
||||
// GetRoleByID gets a role by ID
|
||||
func (s *RBACService) GetRoleByID(ctx context.Context, roleID int64) (*Role, error) {
|
||||
func (s *RBACService) GetRoleByID(ctx context.Context, roleID uuid.UUID) (*Role, error) {
|
||||
query := `
|
||||
SELECT r.id, r.name, r.description, r.is_system, r.created_at, r.updated_at
|
||||
FROM roles r
|
||||
|
|
@ -134,7 +134,7 @@ func (s *RBACService) GetRoleByID(ctx context.Context, roleID int64) (*Role, err
|
|||
}
|
||||
|
||||
// GetRolePermissions gets permissions for a role
|
||||
func (s *RBACService) GetRolePermissions(ctx context.Context, roleID int64) ([]Permission, error) {
|
||||
func (s *RBACService) GetRolePermissions(ctx context.Context, roleID uuid.UUID) ([]Permission, error) {
|
||||
query := `
|
||||
SELECT p.id, p.name, p.description, p.resource, p.action, p.created_at
|
||||
FROM permissions p
|
||||
|
|
@ -164,8 +164,8 @@ func (s *RBACService) GetRolePermissions(ctx context.Context, roleID int64) ([]P
|
|||
}
|
||||
|
||||
// AssignRoleToUser assigns a role to a user
|
||||
// MIGRATION UUID: userID migré vers uuid.UUID, roleID reste int64
|
||||
func (s *RBACService) AssignRoleToUser(ctx context.Context, userID uuid.UUID, roleID int64) error {
|
||||
// MIGRATION UUID: userID migré vers uuid.UUID, roleID aussi
|
||||
func (s *RBACService) AssignRoleToUser(ctx context.Context, userID uuid.UUID, roleID uuid.UUID) error {
|
||||
// Check if user exists
|
||||
var userCount int
|
||||
err := s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE id = $1", userID).Scan(&userCount)
|
||||
|
|
@ -198,20 +198,20 @@ func (s *RBACService) AssignRoleToUser(ctx context.Context, userID uuid.UUID, ro
|
|||
|
||||
// Assign role to user
|
||||
_, err = s.db.ExecContext(ctx, `
|
||||
INSERT INTO user_roles (user_id, role_id, created_at)
|
||||
VALUES ($1, $2, CURRENT_TIMESTAMP)
|
||||
INSERT INTO user_roles (id, user_id, role_id, created_at)
|
||||
VALUES (gen_random_uuid(), $1, $2, CURRENT_TIMESTAMP)
|
||||
`, userID, roleID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to assign role to user: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("Role assigned to user successfully", zap.String("user_id", userID.String()), zap.Int64("role_id", roleID))
|
||||
s.logger.Info("Role assigned to user successfully", zap.String("user_id", userID.String()), zap.String("role_id", roleID.String()))
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveRoleFromUser removes a role from a user
|
||||
// MIGRATION UUID: userID migré vers uuid.UUID, roleID reste int64
|
||||
func (s *RBACService) RemoveRoleFromUser(ctx context.Context, userID uuid.UUID, roleID int64) error {
|
||||
// MIGRATION UUID: userID migré vers uuid.UUID, roleID aussi
|
||||
func (s *RBACService) RemoveRoleFromUser(ctx context.Context, userID uuid.UUID, roleID uuid.UUID) error {
|
||||
result, err := s.db.ExecContext(ctx, `
|
||||
DELETE FROM user_roles
|
||||
WHERE user_id = $1 AND role_id = $2
|
||||
|
|
@ -229,7 +229,7 @@ func (s *RBACService) RemoveRoleFromUser(ctx context.Context, userID uuid.UUID,
|
|||
return fmt.Errorf("role not assigned to user")
|
||||
}
|
||||
|
||||
s.logger.Info("Role removed from user successfully", zap.String("user_id", userID.String()), zap.Int64("role_id", roleID))
|
||||
s.logger.Info("Role removed from user successfully", zap.String("user_id", userID.String()), zap.String("role_id", roleID.String()))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -335,10 +335,10 @@ func (s *RBACService) CreatePermission(ctx context.Context, name, description, r
|
|||
}
|
||||
|
||||
// Create permission
|
||||
var permID int64
|
||||
var permID uuid.UUID
|
||||
query := `
|
||||
INSERT INTO permissions (name, description, resource, action, created_at)
|
||||
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP)
|
||||
INSERT INTO permissions (id, name, description, resource, action, created_at)
|
||||
VALUES (gen_random_uuid(), $1, $2, $3, $4, CURRENT_TIMESTAMP)
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
|
|
@ -394,4 +394,4 @@ func (s *RBACService) GetAllRoles(ctx context.Context) ([]*Role, error) {
|
|||
}
|
||||
|
||||
return roles, nil
|
||||
}
|
||||
}
|
||||
|
|
@ -19,13 +19,13 @@ type SocialService struct {
|
|||
|
||||
// Comment represents a comment on a track
|
||||
type Comment struct {
|
||||
ID int64 `json:"id" db:"id"`
|
||||
UserID int64 `json:"user_id" db:"user_id"`
|
||||
TrackID int64 `json:"track_id" db:"track_id"`
|
||||
ParentID *int64 `json:"parent_id" db:"parent_id"`
|
||||
Content string `json:"content" db:"content"`
|
||||
CreatedAt string `json:"created_at" db:"created_at"`
|
||||
UpdatedAt string `json:"updated_at" db:"updated_at"`
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
UserID uuid.UUID `json:"user_id" db:"user_id"`
|
||||
TrackID uuid.UUID `json:"track_id" db:"track_id"`
|
||||
ParentID *uuid.UUID `json:"parent_id" db:"parent_id"`
|
||||
Content string `json:"content" db:"content"`
|
||||
CreatedAt string `json:"created_at" db:"created_at"`
|
||||
UpdatedAt string `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// NewSocialService creates a new social service
|
||||
|
|
@ -37,7 +37,7 @@ func NewSocialService(db *database.Database, logger *zap.Logger) *SocialService
|
|||
}
|
||||
|
||||
// FollowUser creates a follow relationship
|
||||
func (ss *SocialService) FollowUser(followerID, followedID int64) error {
|
||||
func (ss *SocialService) FollowUser(followerID, followedID uuid.UUID) error {
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := ss.db.ExecContext(ctx, `
|
||||
|
|
@ -51,15 +51,15 @@ func (ss *SocialService) FollowUser(followerID, followedID int64) error {
|
|||
}
|
||||
|
||||
ss.logger.Info("User followed",
|
||||
zap.Int64("follower_id", followerID),
|
||||
zap.Int64("followed_id", followedID),
|
||||
zap.String("follower_id", followerID.String()),
|
||||
zap.String("followed_id", followedID.String()),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnfollowUser removes a follow relationship
|
||||
func (ss *SocialService) UnfollowUser(followerID, followedID int64) error {
|
||||
func (ss *SocialService) UnfollowUser(followerID, followedID uuid.UUID) error {
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := ss.db.ExecContext(ctx, `
|
||||
|
|
@ -75,7 +75,7 @@ func (ss *SocialService) UnfollowUser(followerID, followedID int64) error {
|
|||
}
|
||||
|
||||
// LikeTrack creates a like on a track
|
||||
func (ss *SocialService) LikeTrack(userID, trackID int64) error {
|
||||
func (ss *SocialService) LikeTrack(userID, trackID uuid.UUID) error {
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := ss.db.ExecContext(ctx, `
|
||||
|
|
@ -92,7 +92,7 @@ func (ss *SocialService) LikeTrack(userID, trackID int64) error {
|
|||
}
|
||||
|
||||
// UnlikeTrack removes a like from a track
|
||||
func (ss *SocialService) UnlikeTrack(userID, trackID int64) error {
|
||||
func (ss *SocialService) UnlikeTrack(userID, trackID uuid.UUID) error {
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := ss.db.ExecContext(ctx, `
|
||||
|
|
@ -108,13 +108,13 @@ func (ss *SocialService) UnlikeTrack(userID, trackID int64) error {
|
|||
}
|
||||
|
||||
// CreateComment creates a comment on a track
|
||||
func (ss *SocialService) CreateComment(userID, trackID int64, content string, parentID *int64) (*Comment, error) {
|
||||
func (ss *SocialService) CreateComment(userID, trackID uuid.UUID, content string, parentID *uuid.UUID) (*Comment, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
var commentID int64
|
||||
var commentID uuid.UUID
|
||||
err := ss.db.QueryRowContext(ctx, `
|
||||
INSERT INTO comments (user_id, track_id, parent_id, content)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
INSERT INTO comments (id, user_id, track_id, parent_id, content)
|
||||
VALUES (gen_random_uuid(), $1, $2, $3, $4)
|
||||
RETURNING id
|
||||
`, userID, trackID, parentID, content).Scan(&commentID)
|
||||
|
||||
|
|
@ -182,7 +182,7 @@ func (ss *SocialService) GetFollowingCount(userID uuid.UUID) (int, error) {
|
|||
}
|
||||
|
||||
// GetLikesCount returns the number of likes for a track
|
||||
func (ss *SocialService) GetLikesCount(trackID int64) (int, error) {
|
||||
func (ss *SocialService) GetLikesCount(trackID uuid.UUID) (int, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
var count int
|
||||
|
|
@ -200,7 +200,7 @@ func (ss *SocialService) GetLikesCount(trackID int64) (int, error) {
|
|||
}
|
||||
|
||||
// IsFollowing checks if a user is following another user
|
||||
func (ss *SocialService) IsFollowing(followerID, followedID int64) (bool, error) {
|
||||
func (ss *SocialService) IsFollowing(followerID, followedID uuid.UUID) (bool, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
var exists bool
|
||||
|
|
@ -222,7 +222,7 @@ func (ss *SocialService) IsFollowing(followerID, followedID int64) (bool, error)
|
|||
}
|
||||
|
||||
// IsTrackLiked checks if a user has liked a track
|
||||
func (ss *SocialService) IsTrackLiked(userID, trackID int64) (bool, error) {
|
||||
func (ss *SocialService) IsTrackLiked(userID, trackID uuid.UUID) (bool, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
var exists bool
|
||||
|
|
@ -241,4 +241,4 @@ func (ss *SocialService) IsTrackLiked(userID, trackID int64) (bool, error) {
|
|||
}
|
||||
|
||||
return exists, nil
|
||||
}
|
||||
}
|
||||
|
|
@ -31,7 +31,7 @@ type PaginationResponse struct {
|
|||
|
||||
// Cursor représente un curseur de pagination
|
||||
type Cursor struct {
|
||||
ID int64 `json:"id"`
|
||||
ID string `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ func DecodeCursor(cursorStr string) (*Cursor, error) {
|
|||
}
|
||||
|
||||
// CreateCursor crée un nouveau curseur à partir d'un ID et d'une date
|
||||
func CreateCursor(id int64, createdAt time.Time) *Cursor {
|
||||
func CreateCursor(id string, createdAt time.Time) *Cursor {
|
||||
return &Cursor{
|
||||
ID: id,
|
||||
CreatedAt: createdAt,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
-- Migration: Finaliser la transition UUID pour les tables secondaires
|
||||
-- Date: 2024-12-04
|
||||
-- Description: Finalise le travail de migrations/001_migrate_ids_to_uuid_up.sql
|
||||
-- Pour chaque table ayant une colonne 'new_id' (UUID) et une ancienne 'id' (INT/BIGINT),
|
||||
-- on supprime l'ancienne et on promeut la nouvelle en Primary Key.
|
||||
|
||||
-- Liste des tables concernées:
|
||||
-- hls_transcode_queue, bitrate_adaptation_logs, contests, contest_entries, contest_judges,
|
||||
-- contest_votes, contest_sponsors, contest_stems, contest_analytics, contest_badges,
|
||||
-- federated_identities, equipment, hardware_sales, equipment_trades, hardware_offers,
|
||||
-- mfa_configs, playback_analytics, recovery_codes, refresh_tokens, roles, permissions,
|
||||
-- user_roles, royalty_records, royalty_payouts, royalty_rates, creator_royalty_rates,
|
||||
-- royalty_config, sellable_contents, jury_members, track_history, track_versions,
|
||||
-- user_settings, user_profiles
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
tables text[] := ARRAY[
|
||||
'hls_transcode_queue', 'bitrate_adaptation_logs', 'contests', 'contest_entries',
|
||||
'contest_judges', 'contest_votes', 'contest_sponsors', 'contest_stems',
|
||||
'contest_analytics', 'contest_badges', 'federated_identities', 'equipment',
|
||||
'hardware_sales', 'equipment_trades', 'hardware_offers', 'mfa_configs',
|
||||
'playback_analytics', 'recovery_codes', 'refresh_tokens', 'roles', 'permissions',
|
||||
'user_roles', 'royalty_records', 'royalty_payouts', 'royalty_rates',
|
||||
'creator_royalty_rates', 'royalty_config', 'sellable_contents', 'jury_members',
|
||||
'track_history', 'track_versions', 'user_settings', 'user_profiles'
|
||||
];
|
||||
t text;
|
||||
BEGIN
|
||||
FOREACH t IN ARRAY tables LOOP
|
||||
-- Vérifier si la table existe et a la colonne new_id
|
||||
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = t AND column_name = 'new_id') THEN
|
||||
|
||||
-- 1. Supprimer l'ancienne contrainte de clé primaire (si elle existe)
|
||||
-- On cherche le nom de la contrainte PK
|
||||
EXECUTE 'ALTER TABLE ' || quote_ident(t) || ' DROP CONSTRAINT IF EXISTS ' || quote_ident(t || '_pkey') || ' CASCADE';
|
||||
|
||||
-- 2. Supprimer l'ancienne colonne ID
|
||||
EXECUTE 'ALTER TABLE ' || quote_ident(t) || ' DROP COLUMN IF EXISTS id CASCADE';
|
||||
|
||||
-- 3. Renommer new_id en id
|
||||
EXECUTE 'ALTER TABLE ' || quote_ident(t) || ' RENAME COLUMN new_id TO id';
|
||||
|
||||
-- 4. Mettre id en NOT NULL (devrait déjà l'être via default, mais sécurité)
|
||||
EXECUTE 'ALTER TABLE ' || quote_ident(t) || ' ALTER COLUMN id SET NOT NULL';
|
||||
|
||||
-- 5. Ajouter la nouvelle clé primaire
|
||||
EXECUTE 'ALTER TABLE ' || quote_ident(t) || ' ADD PRIMARY KEY (id)';
|
||||
|
||||
RAISE NOTICE 'Table % migrée avec succès vers UUID', t;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
-- Migration: Migrer PK tracks et playlists vers UUID
|
||||
-- Date: 2024-12-04
|
||||
-- Description: S'assure que les tables tracks et playlists utilisent bien UUID comme clé primaire.
|
||||
-- Si elles sont encore en SERIAL/INT, on effectue la migration.
|
||||
|
||||
-- Extension requise (déjà là normalement)
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
-- =================================================================
|
||||
-- TRACKS
|
||||
-- =================================================================
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
col_type text;
|
||||
BEGIN
|
||||
-- Vérifier le type actuel de la colonne id dans tracks
|
||||
SELECT data_type INTO col_type FROM information_schema.columns
|
||||
WHERE table_name = 'tracks' AND column_name = 'id';
|
||||
|
||||
-- Si ce n'est pas déjà un UUID, on migre
|
||||
IF col_type != 'uuid' THEN
|
||||
RAISE NOTICE 'Migration tracks.id de % vers UUID', col_type;
|
||||
|
||||
-- 1. Ajouter colonne temporaire
|
||||
ALTER TABLE tracks ADD COLUMN IF NOT EXISTS id_uuid UUID DEFAULT uuid_generate_v4();
|
||||
|
||||
-- 2. Populer (déjà fait par défaut, mais update pour être sûr)
|
||||
UPDATE tracks SET id_uuid = uuid_generate_v4() WHERE id_uuid IS NULL;
|
||||
|
||||
-- 3. Dropper contraintes dépendantes (FKs pointant vers tracks)
|
||||
-- Ex: playlist_tracks, track_likes, track_comments, track_shares, hls_streams...
|
||||
-- NOTE: En prod, il faudrait migrer les FKs de ces tables aussi !
|
||||
-- Pour cette phase, on suppose que les tables liées sont gérées par GORM ou seront fixées.
|
||||
-- Simplification: On drop la PK, on switch, on remet la PK.
|
||||
|
||||
ALTER TABLE tracks DROP CONSTRAINT IF EXISTS tracks_pkey CASCADE;
|
||||
|
||||
-- 4. Swap
|
||||
ALTER TABLE tracks DROP COLUMN id;
|
||||
ALTER TABLE tracks RENAME COLUMN id_uuid TO id;
|
||||
|
||||
-- 5. PK
|
||||
ALTER TABLE tracks ADD PRIMARY KEY (id);
|
||||
|
||||
ELSE
|
||||
RAISE NOTICE 'tracks.id est déjà un UUID. Pas de changement.';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- =================================================================
|
||||
-- PLAYLISTS
|
||||
-- =================================================================
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
col_type text;
|
||||
BEGIN
|
||||
SELECT data_type INTO col_type FROM information_schema.columns
|
||||
WHERE table_name = 'playlists' AND column_name = 'id';
|
||||
|
||||
IF col_type != 'uuid' THEN
|
||||
RAISE NOTICE 'Migration playlists.id de % vers UUID', col_type;
|
||||
|
||||
ALTER TABLE playlists ADD COLUMN IF NOT EXISTS id_uuid UUID DEFAULT uuid_generate_v4();
|
||||
UPDATE playlists SET id_uuid = uuid_generate_v4() WHERE id_uuid IS NULL;
|
||||
|
||||
ALTER TABLE playlists DROP CONSTRAINT IF EXISTS playlists_pkey CASCADE;
|
||||
|
||||
ALTER TABLE playlists DROP COLUMN id;
|
||||
ALTER TABLE playlists RENAME COLUMN id_uuid TO id;
|
||||
|
||||
ALTER TABLE playlists ADD PRIMARY KEY (id);
|
||||
ELSE
|
||||
RAISE NOTICE 'playlists.id est déjà un UUID. Pas de changement.';
|
||||
END IF;
|
||||
END $$;
|
||||
13
veza-backend-api/migrations/072_create_chat_schema.sql
Normal file
13
veza-backend-api/migrations/072_create_chat_schema.sql
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
-- Migration: Création du schéma Chat
|
||||
-- Date: 2024-12-04
|
||||
-- Description: Crée le schéma isolé pour le Chat Server afin d'éviter les conflits de tables.
|
||||
|
||||
-- Créer le schéma s'il n'existe pas
|
||||
CREATE SCHEMA IF NOT EXISTS chat;
|
||||
|
||||
-- Commentaire explicatif
|
||||
COMMENT ON SCHEMA chat IS 'Schéma isolé pour les données du Veza Chat Server (Rust)';
|
||||
|
||||
-- Droits (Optionnel selon la config user DB, mais recommandé)
|
||||
-- GRANT ALL ON SCHEMA chat TO veza_user;
|
||||
-- GRANT ALL ON ALL TABLES IN SCHEMA chat TO veza_user;
|
||||
Loading…
Reference in a new issue