P0 UUID Phase A: migrations + backend Go UUID refactor

This commit is contained in:
okinrev 2025-12-04 02:15:48 +01:00
parent 327ac36a30
commit 65420e7c0d
15 changed files with 651 additions and 86 deletions

30
CLEANUP_PLAN.md Normal file
View 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
View 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
View 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
View 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
View 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.

View 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.

View 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.

View file

@ -152,14 +152,14 @@ func createDummyAudioFile() (string, error) {
type UploadResponse struct { type UploadResponse struct {
Message string `json:"message"` Message string `json:"message"`
Track struct { Track struct {
ID int64 `json:"id"` ID string `json:"id"`
} `json:"track"` } `json:"track"`
} }
func uploadTrack(token, filePath string) (int64, error) { func uploadTrack(token, filePath string) (string, error) {
file, err := os.Open(filePath) file, err := os.Open(filePath)
if err != nil { if err != nil {
return 0, err return "", err
} }
defer file.Close() defer file.Close()
@ -174,7 +174,7 @@ func uploadTrack(token, filePath string) (int64, error) {
req, err := http.NewRequest("POST", baseURL+"/tracks/upload", body) req, err := http.NewRequest("POST", baseURL+"/tracks/upload", body)
if err != nil { if err != nil {
return 0, err return "", err
} }
req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("Content-Type", writer.FormDataContentType())
req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Authorization", "Bearer "+token)
@ -182,23 +182,23 @@ func uploadTrack(token, filePath string) (int64, error) {
client := &http.Client{Timeout: 30 * time.Second} client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return 0, err return "", err
} }
defer resp.Body.Close() defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body) respBody, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusCreated { 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 var result UploadResponse
if err := json.Unmarshal(respBody, &result); err != nil { 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 { if result.Track.ID == "" {
return 0, fmt.Errorf("no track ID returned") return "", fmt.Errorf("no track ID returned")
} }
return result.Track.ID, nil return result.Track.ID, nil
@ -208,12 +208,12 @@ type TrackStatusResponse struct {
Status string `json:"status"` 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} client := &http.Client{Timeout: 5 * time.Second}
maxRetries := 5 // Short wait maxRetries := 5 // Short wait
for i := 0; i < maxRetries; i++ { 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 { if err != nil {
return err return err
} }
@ -250,12 +250,12 @@ func waitForProcessing(token string, trackID int64) error {
return fmt.Errorf("track did not reach stable state") 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. // Check if we can get the track details.
// The actual stream URL might be in the track details or a separate endpoint. // The actual stream URL might be in the track details or a separate endpoint.
// Based on API Spec: GET /tracks/:id // 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 { if err != nil {
return err return err
} }

View file

@ -32,19 +32,20 @@ type OAuthService struct {
} }
// OAuthAccount represents an OAuth account linking // OAuthAccount represents an OAuth account linking
// Mapped to federated_identities table
type OAuthAccount struct { type OAuthAccount struct {
ID int64 `json:"id" db:"id"` ID uuid.UUID `json:"id" db:"id"`
UserID uuid.UUID `json:"user_id" db:"user_id"` UserID uuid.UUID `json:"user_id" db:"user_id"`
Provider string `json:"provider" db:"provider"` Provider string `json:"provider" db:"provider"`
ProviderUserID string `json:"provider_user_id" db:"provider_user_id"` ProviderID string `json:"provider_id" db:"provider_id"`
Email string `json:"email" db:"email"` Email string `json:"email" db:"email"`
Name string `json:"name" db:"name"` DisplayName string `json:"display_name" db:"display_name"`
AvatarURL string `json:"avatar_url" db:"avatar_url"` AvatarURL string `json:"avatar_url" db:"avatar_url"`
AccessToken string `json:"-" db:"access_token"` AccessToken string `json:"-" db:"access_token"`
RefreshToken string `json:"-" db:"refresh_token"` RefreshToken string `json:"-" db:"refresh_token"`
ExpiresAt time.Time `json:"expires_at" db:"expires_at"` ExpiresAt time.Time `json:"expires_at" db:"expires_at"`
CreatedAt time.Time `json:"created_at" db:"created_at"` CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
} }
// OAuthState represents an OAuth state for CSRF protection // 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 // saveOAuthAccount saves or updates OAuth account information
// Uses federated_identities table
func (os *OAuthService) saveOAuthAccount(oauthUser *OAuthUser, userID uuid.UUID, token *oauth2.Token) error { func (os *OAuthService) saveOAuthAccount(oauthUser *OAuthUser, userID uuid.UUID, token *oauth2.Token) error {
ctx := context.Background() ctx := context.Background()
// Check if OAuth account already exists // Check if OAuth account already exists
var existingID int64 var existingID uuid.UUID
err := os.db.QueryRowContext(ctx, ` err := os.db.QueryRowContext(ctx, `
SELECT id FROM oauth_accounts SELECT id FROM federated_identities
WHERE user_id = $1 AND provider_user_id = $2 WHERE user_id = $1 AND provider_id = $2
`, userID, oauthUser.ProviderID).Scan(&existingID) `, userID, oauthUser.ProviderID).Scan(&existingID)
if err == nil { if err == nil {
// Update existing // Update existing
_, err = os.db.ExecContext(ctx, ` _, err = os.db.ExecContext(ctx, `
UPDATE oauth_accounts UPDATE federated_identities
SET email = $1, name = $2, access_token = $3, refresh_token = $4, expires_at = $5, updated_at = NOW() SET email = $1, display_name = $2, access_token = $3, refresh_token = $4, expires_at = $5, updated_at = NOW()
WHERE id = $6 WHERE id = $6
`, oauthUser.Email, oauthUser.Name, token.AccessToken, token.RefreshToken, token.Expiry, existingID) `, oauthUser.Email, oauthUser.Name, token.AccessToken, token.RefreshToken, token.Expiry, existingID)
return err return err
@ -459,8 +461,8 @@ func (os *OAuthService) saveOAuthAccount(oauthUser *OAuthUser, userID uuid.UUID,
// Insert new // Insert new
_, err = os.db.ExecContext(ctx, ` _, 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) 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 ($1, $2, $3, $4, $5, $6, $7, $8, $9) 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) `, userID, "oauth", oauthUser.ProviderID, oauthUser.Email, oauthUser.Name, oauthUser.Avatar, token.AccessToken, token.RefreshToken, token.Expiry)
return err return err
@ -475,4 +477,4 @@ func (os *OAuthService) generateJWT(userID uuid.UUID) (string, error) {
}) })
return token.SignedString(os.jwtSecret) return token.SignedString(os.jwtSecret)
} }

View file

@ -27,7 +27,7 @@ func NewRBACService(db *database.Database, logger *zap.Logger) *RBACService {
// Role represents a user role // Role represents a user role
type Role struct { type Role struct {
ID int64 `json:"id"` ID uuid.UUID `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
Permissions []Permission `json:"permissions"` Permissions []Permission `json:"permissions"`
@ -38,24 +38,24 @@ type Role struct {
// Permission represents a permission // Permission represents a permission
type Permission struct { type Permission struct {
ID int64 `json:"id"` ID uuid.UUID `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
Resource string `json:"resource"` Resource string `json:"resource"`
Action string `json:"action"` Action string `json:"action"`
CreatedAt string `json:"created_at"` CreatedAt string `json:"created_at"`
} }
// UserRole represents a user's role assignment // UserRole represents a user's role assignment
type UserRole struct { type UserRole struct {
ID int64 `json:"id"` ID uuid.UUID `json:"id"`
UserID int64 `json:"user_id"` UserID uuid.UUID `json:"user_id"`
RoleID int64 `json:"role_id"` RoleID uuid.UUID `json:"role_id"`
Role *Role `json:"role,omitempty"` Role *Role `json:"role,omitempty"`
} }
// CreateRole creates a new role // 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 // Check if role already exists
var count int var count int
err := s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM roles WHERE name = $1", name).Scan(&count) 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 // Create role
var roleID int64 var roleID uuid.UUID
query := ` query := `
INSERT INTO roles (name, description, is_system, created_at, updated_at) INSERT INTO roles (id, name, description, is_system, created_at, updated_at)
VALUES ($1, $2, false, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) VALUES (gen_random_uuid(), $1, $2, false, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
RETURNING id 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) 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 return role, nil
} }
// GetRoleByID gets a role by ID // 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 := ` query := `
SELECT r.id, r.name, r.description, r.is_system, r.created_at, r.updated_at SELECT r.id, r.name, r.description, r.is_system, r.created_at, r.updated_at
FROM roles r FROM roles r
@ -134,7 +134,7 @@ func (s *RBACService) GetRoleByID(ctx context.Context, roleID int64) (*Role, err
} }
// GetRolePermissions gets permissions for a role // 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 := ` query := `
SELECT p.id, p.name, p.description, p.resource, p.action, p.created_at SELECT p.id, p.name, p.description, p.resource, p.action, p.created_at
FROM permissions p FROM permissions p
@ -164,8 +164,8 @@ func (s *RBACService) GetRolePermissions(ctx context.Context, roleID int64) ([]P
} }
// AssignRoleToUser assigns a role to a user // AssignRoleToUser assigns a role to a user
// MIGRATION UUID: userID migré vers uuid.UUID, roleID reste int64 // MIGRATION UUID: userID migré vers uuid.UUID, roleID aussi
func (s *RBACService) AssignRoleToUser(ctx context.Context, userID uuid.UUID, roleID int64) error { func (s *RBACService) AssignRoleToUser(ctx context.Context, userID uuid.UUID, roleID uuid.UUID) error {
// Check if user exists // Check if user exists
var userCount int var userCount int
err := s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE id = $1", userID).Scan(&userCount) 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 // Assign role to user
_, err = s.db.ExecContext(ctx, ` _, err = s.db.ExecContext(ctx, `
INSERT INTO user_roles (user_id, role_id, created_at) INSERT INTO user_roles (id, user_id, role_id, created_at)
VALUES ($1, $2, CURRENT_TIMESTAMP) VALUES (gen_random_uuid(), $1, $2, CURRENT_TIMESTAMP)
`, userID, roleID) `, userID, roleID)
if err != nil { if err != nil {
return fmt.Errorf("failed to assign role to user: %w", err) 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 return nil
} }
// RemoveRoleFromUser removes a role from a user // RemoveRoleFromUser removes a role from a user
// MIGRATION UUID: userID migré vers uuid.UUID, roleID reste int64 // MIGRATION UUID: userID migré vers uuid.UUID, roleID aussi
func (s *RBACService) RemoveRoleFromUser(ctx context.Context, userID uuid.UUID, roleID int64) error { func (s *RBACService) RemoveRoleFromUser(ctx context.Context, userID uuid.UUID, roleID uuid.UUID) error {
result, err := s.db.ExecContext(ctx, ` result, err := s.db.ExecContext(ctx, `
DELETE FROM user_roles DELETE FROM user_roles
WHERE user_id = $1 AND role_id = $2 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") 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 return nil
} }
@ -335,10 +335,10 @@ func (s *RBACService) CreatePermission(ctx context.Context, name, description, r
} }
// Create permission // Create permission
var permID int64 var permID uuid.UUID
query := ` query := `
INSERT INTO permissions (name, description, resource, action, created_at) INSERT INTO permissions (id, name, description, resource, action, created_at)
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP) VALUES (gen_random_uuid(), $1, $2, $3, $4, CURRENT_TIMESTAMP)
RETURNING id RETURNING id
` `
@ -394,4 +394,4 @@ func (s *RBACService) GetAllRoles(ctx context.Context) ([]*Role, error) {
} }
return roles, nil return roles, nil
} }

View file

@ -19,13 +19,13 @@ type SocialService struct {
// Comment represents a comment on a track // Comment represents a comment on a track
type Comment struct { type Comment struct {
ID int64 `json:"id" db:"id"` ID uuid.UUID `json:"id" db:"id"`
UserID int64 `json:"user_id" db:"user_id"` UserID uuid.UUID `json:"user_id" db:"user_id"`
TrackID int64 `json:"track_id" db:"track_id"` TrackID uuid.UUID `json:"track_id" db:"track_id"`
ParentID *int64 `json:"parent_id" db:"parent_id"` ParentID *uuid.UUID `json:"parent_id" db:"parent_id"`
Content string `json:"content" db:"content"` Content string `json:"content" db:"content"`
CreatedAt string `json:"created_at" db:"created_at"` CreatedAt string `json:"created_at" db:"created_at"`
UpdatedAt string `json:"updated_at" db:"updated_at"` UpdatedAt string `json:"updated_at" db:"updated_at"`
} }
// NewSocialService creates a new social service // NewSocialService creates a new social service
@ -37,7 +37,7 @@ func NewSocialService(db *database.Database, logger *zap.Logger) *SocialService
} }
// FollowUser creates a follow relationship // 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() ctx := context.Background()
_, err := ss.db.ExecContext(ctx, ` _, err := ss.db.ExecContext(ctx, `
@ -51,15 +51,15 @@ func (ss *SocialService) FollowUser(followerID, followedID int64) error {
} }
ss.logger.Info("User followed", ss.logger.Info("User followed",
zap.Int64("follower_id", followerID), zap.String("follower_id", followerID.String()),
zap.Int64("followed_id", followedID), zap.String("followed_id", followedID.String()),
) )
return nil return nil
} }
// UnfollowUser removes a follow relationship // 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() ctx := context.Background()
_, err := ss.db.ExecContext(ctx, ` _, err := ss.db.ExecContext(ctx, `
@ -75,7 +75,7 @@ func (ss *SocialService) UnfollowUser(followerID, followedID int64) error {
} }
// LikeTrack creates a like on a track // 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() ctx := context.Background()
_, err := ss.db.ExecContext(ctx, ` _, err := ss.db.ExecContext(ctx, `
@ -92,7 +92,7 @@ func (ss *SocialService) LikeTrack(userID, trackID int64) error {
} }
// UnlikeTrack removes a like from a track // 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() ctx := context.Background()
_, err := ss.db.ExecContext(ctx, ` _, err := ss.db.ExecContext(ctx, `
@ -108,13 +108,13 @@ func (ss *SocialService) UnlikeTrack(userID, trackID int64) error {
} }
// CreateComment creates a comment on a track // 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() ctx := context.Background()
var commentID int64 var commentID uuid.UUID
err := ss.db.QueryRowContext(ctx, ` err := ss.db.QueryRowContext(ctx, `
INSERT INTO comments (user_id, track_id, parent_id, content) INSERT INTO comments (id, user_id, track_id, parent_id, content)
VALUES ($1, $2, $3, $4) VALUES (gen_random_uuid(), $1, $2, $3, $4)
RETURNING id RETURNING id
`, userID, trackID, parentID, content).Scan(&commentID) `, 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 // 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() ctx := context.Background()
var count int var count int
@ -200,7 +200,7 @@ func (ss *SocialService) GetLikesCount(trackID int64) (int, error) {
} }
// IsFollowing checks if a user is following another user // 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() ctx := context.Background()
var exists bool 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 // 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() ctx := context.Background()
var exists bool var exists bool
@ -241,4 +241,4 @@ func (ss *SocialService) IsTrackLiked(userID, trackID int64) (bool, error) {
} }
return exists, nil return exists, nil
} }

View file

@ -31,7 +31,7 @@ type PaginationResponse struct {
// Cursor représente un curseur de pagination // Cursor représente un curseur de pagination
type Cursor struct { type Cursor struct {
ID int64 `json:"id"` ID string `json:"id"`
CreatedAt time.Time `json:"created_at"` 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 // 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{ return &Cursor{
ID: id, ID: id,
CreatedAt: createdAt, CreatedAt: createdAt,

View file

@ -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 $$;

View file

@ -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 $$;

View 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;