Feat: Système de migrations automatiques au démarrage

Implémente un système de migrations automatiques qui s'exécute à chaque
démarrage de l'application Docker/Node.

Problème résolu :
- Les utilisateurs n'ont plus besoin d'exécuter manuellement les migrations
- Les nouveaux champs SQL sont ajoutés automatiquement
- Évite les erreurs "column does not exist"
- Garantit la cohérence entre le code et le schéma DB

Fonctionnement :
1. Au démarrage : initDatabase() → autoMigrate() → startSchedulers()
2. autoMigrate() vérifie chaque colonne via information_schema
3. Si la colonne n'existe pas, elle est ajoutée automatiquement
4. Les triggers et index sont également créés/mis à jour

Migrations incluses :
- Migration 1 : Champs de tracking (archived_at, completed_at)
  * notes.archived_at
  * global_todos.completed_at
  * note_todos.completed_at + created_at
  * Triggers automatiques

- Migration 2 : Champ priority pour tâches
  * global_todos.priority
  * note_todos.priority
  * Index de performance

Caractéristiques :
- ✅ Idempotent : Peut s'exécuter plusieurs fois sans problème
- ✅ Transactionnel : Utilise BEGIN/COMMIT/ROLLBACK
- ✅ Non-bloquant : L'application démarre même en cas d'erreur
- ✅ Intelligent : Vérifie avant de créer
- ✅ Sécurisé : Double protection avec rétrocompatibilité API
- ✅ Documenté : Guide complet dans docs/MIGRATIONS.md

Fichiers créés :
- scripts/auto-migrate.js - Script de migrations automatiques
- docs/MIGRATIONS.md - Documentation complète

Fichiers modifiés :
- server.js - Appel de autoMigrate() au démarrage

Logs au démarrage :
✓ Base de données initialisée avec succès
🔄 Vérification des migrations...
  → Ajout du champ priority à global_todos (si nécessaire)
✅ Migrations automatiques terminées avec succès
✓ Migrations automatiques appliquées

Impact :
Les utilisateurs peuvent maintenant simplement démarrer l'application avec
docker-compose up et tous les champs SQL seront automatiquement créés.
Plus besoin de lancer des scripts de migration manuellement !
This commit is contained in:
Claude committed 2025-11-21 14:19:33 +00:00
1 parent 941493cb16
commit 3677bdfe70
3 files changed
+504

No files matched your search

+295
View File
@@ -0,0 +1,295 @@
# Système de Migrations Automatiques
Ce document décrit le système de migrations automatiques de NoteFlow.
## Vue d'ensemble
Le système de migrations automatiques s'exécute **à chaque démarrage de l'application** pour s'assurer que le schéma de la base de données est à jour. Cela permet de :
- ✅ Déployer automatiquement les changements de schéma
- ✅ Éviter les erreurs de "colonne inexistante"
- ✅ Simplifier le processus de mise à jour
- ✅ Garantir la cohérence entre le code et la base de données
## Comment ça fonctionne
### 1. Démarrage de l'application
Lors du démarrage, l'application exécute dans l'ordre :
```javascript
1. initDatabase() // Crée les tables de base
2. autoMigrate() // Applique les migrations manquantes
3. startSchedulers() // Démarre les services
4. listen() // Lance le serveur
```
### 2. Vérification intelligente
Le script `scripts/auto-migrate.js` vérifie pour chaque migration :
```sql
-- Est-ce que la colonne existe déjà ?
SELECT column_name
FROM information_schema.columns
WHERE table_name='ma_table' AND column_name='ma_colonne'
```
Si la colonne n'existe pas, elle est créée automatiquement.
### 3. Sécurité
- **Idempotent** : Les migrations peuvent être exécutées plusieurs fois sans problème
- **Transactionnel** : Utilise `BEGIN`/`COMMIT`/`ROLLBACK`
- **Non-bloquant** : En cas d'erreur, l'application démarre quand même (mais log l'erreur)
- **Double protection** : Le code API a aussi une rétrocompatibilité en cas d'échec
## Migrations actuelles
### Migration 1 : Champs de tracking pour purge
**Ajouté** : v1.1.0
**Objectif** : Permettre la purge automatique des données obsolètes
**Changements** :
- `notes.archived_at` - Date d'archivage d'une note
- `global_todos.completed_at` - Date de complétion d'une tâche globale
- `note_todos.completed_at` - Date de complétion d'une tâche de note
- `note_todos.created_at` - Date de création d'une tâche de note
- Triggers PostgreSQL pour mise à jour automatique
**Script manuel** (si besoin) :
```bash
npm run db:migrate
```
### Migration 2 : Champ priority pour tâches
**Ajouté** : v1.2.0
**Objectif** : Permettre de marquer les tâches importantes avec une étoile
**Changements** :
- `global_todos.priority` - Indicateur de priorité (BOOLEAN)
- `note_todos.priority` - Indicateur de priorité (BOOLEAN)
- Index pour optimiser le tri par priorité
**Script manuel** (si besoin) :
```bash
npm run db:migrate:priority
```
## Ajouter une nouvelle migration
Pour ajouter une nouvelle migration au système automatique :
### 1. Créer le script manuel (optionnel)
Créez un fichier dans `scripts/` pour permettre l'exécution manuelle :
```javascript
// scripts/add-mon-champ.js
#!/usr/bin/env node
const { Pool } = require('pg');
// ... votre migration
```
### 2. Ajouter au script auto-migrate.js
Éditez `scripts/auto-migrate.js` et ajoutez votre migration :
```javascript
// Migration 3: Votre nouvelle fonctionnalité
logger.info(' Vérification: mon nouveau champ...');
const monChampExists = await client.query(`
SELECT column_name
FROM information_schema.columns
WHERE table_name='ma_table' AND column_name='mon_champ'
`);
if (monChampExists.rows.length === 0) {
logger.info(' → Ajout du champ mon_champ à ma_table');
await client.query(`ALTER TABLE ma_table ADD COLUMN mon_champ TYPE DEFAULT valeur`);
// Mise à jour des données existantes si nécessaire
await client.query(`UPDATE ma_table SET mon_champ = ... WHERE ...`);
}
```
### 3. Tester localement
```bash
# Démarrer l'application
npm run start
# Vérifier les logs
# Vous devriez voir : "✓ Migrations automatiques appliquées"
# Vérifier que le champ existe
psql $DATABASE_URL -c "\d ma_table"
```
### 4. Ajouter un script npm (optionnel)
Dans `package.json` :
```json
{
"scripts": {
"db:migrate:mon-feature": "node scripts/add-mon-champ.js"
}
}
```
## Dépannage
### La migration ne s'exécute pas
**Vérifiez les logs au démarrage** :
```bash
docker logs noteflow-notes-app-1 | grep -i migration
```
Vous devriez voir :
```
✓ Base de données initialisée avec succès
🔄 Vérification des migrations...
Vérification: champs de tracking pour purge...
Vérification: champ priority pour tâches...
✅ Migrations automatiques terminées avec succès
✓ Migrations automatiques appliquées
```
### Erreur lors de la migration
Les erreurs de migration sont **loggées mais ne bloquent pas le démarrage** :
```
❌ Erreur lors des migrations automatiques: ...
✓ Scheduler RSS démarré
```
Pour corriger :
1. Identifiez l'erreur dans les logs
2. Corrigez le problème (droits, syntaxe SQL, etc.)
3. Redémarrez l'application
### Forcer une migration manuelle
Si vous préférez exécuter manuellement :
```bash
# Dans le conteneur Docker
docker exec -it noteflow-notes-app-1 node scripts/auto-migrate.js
# Ou avec npm
docker exec -it noteflow-notes-app-1 npm run db:migrate
docker exec -it noteflow-notes-app-1 npm run db:migrate:priority
```
### Vérifier l'état des migrations
Utilisez les commandes SQL directement :
```bash
docker exec -it noteflow-postgres-1 psql -U noteflow noteflow
-- Vérifier les colonnes d'une table
\d notes
\d global_todos
\d note_todos
-- Vérifier une colonne spécifique
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_name='global_todos';
-- Vérifier les triggers
SELECT trigger_name, event_manipulation, event_object_table
FROM information_schema.triggers;
```
## Bonnes pratiques
### ✅ À faire
- **Toujours** utiliser `IF NOT EXISTS` / `IF EXISTS`
- **Toujours** tester en local avant de déployer
- **Toujours** ajouter des logs explicites
- **Toujours** gérer la rétrocompatibilité dans le code
- **Toujours** utiliser des transactions (`BEGIN`/`COMMIT`)
- **Toujours** mettre à jour cette documentation
### ❌ À éviter
- Supprimer des colonnes directement (préférer un soft-delete)
- Modifier le type d'une colonne avec données
- Faire des migrations lourdes au démarrage (>5 secondes)
- Oublier les valeurs par défaut pour les colonnes existantes
- Crasher l'application en cas d'erreur de migration
## Architecture
```
📁 noteflow/
├── 📁 scripts/
│ ├── auto-migrate.js ← Script principal (s'exécute au démarrage)
│ ├── add-cleanup-tracking-fields.js ← Migration manuelle 1
│ ├── add-priority-field.js ← Migration manuelle 2
│ └── ... ← Futures migrations
├── 📁 config/
│ └── database-postgres.js ← Schéma initial (CREATE TABLE)
├── server.js ← Appelle autoMigrate() au démarrage
└── package.json ← Scripts npm pour migrations manuelles
```
## Flux de démarrage
```
┌─────────────────────────────────────────────────┐
│ docker-compose up / npm start │
└──────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ server.js: startServer() │
└──────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ initDatabase() │
│ → Crée les tables de base (si n'existent pas) │
└──────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ autoMigrate() │
│ → Vérifie et ajoute les colonnes manquantes │
│ → Crée les triggers │
│ → Crée les index │
└──────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Démarrage des services │
│ → RSS Scheduler │
│ → Cleanup Scheduler │
└──────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ app.listen() - Serveur prêt ! 🚀 │
└─────────────────────────────────────────────────┘
```
## Support
Pour plus d'informations :
- Consultez les logs : `docker logs noteflow-notes-app-1`
- Exécutez manuellement : `node scripts/auto-migrate.js`
- Vérifiez la base : `psql $DATABASE_URL -c "\d"`
+204
View File
@@ -0,0 +1,204 @@
#!/usr/bin/env node
/**
* Script de migrations automatiques
* S'exécute au démarrage de l'application pour mettre à jour le schéma
*/
const { Pool } = require('pg');
const logger = require('../config/logger');
const DATABASE_URL = process.env.DATABASE_URL ||
`postgresql://${process.env.PGUSER || 'noteflow'}:${process.env.PGPASSWORD || 'noteflow_secure_password_change_me'}@${process.env.PGHOST || 'postgres'}:${process.env.PGPORT || '5499'}/${process.env.PGDATABASE || 'noteflow'}`;
async function autoMigrate() {
const pool = new Pool({ connectionString: DATABASE_URL });
try {
logger.info('🔄 Vérification des migrations...');
const client = await pool.connect();
try {
await client.query('BEGIN');
// Migration 1: Champs de tracking pour la purge (archived_at, completed_at)
logger.info(' Vérification: champs de tracking pour purge...');
// Vérifier si archived_at existe
const archivedAtExists = await client.query(`
SELECT column_name
FROM information_schema.columns
WHERE table_name='notes' AND column_name='archived_at'
`);
if (archivedAtExists.rows.length === 0) {
logger.info(' → Ajout du champ archived_at à notes');
await client.query(`ALTER TABLE notes ADD COLUMN archived_at TIMESTAMP`);
await client.query(`UPDATE notes SET archived_at = updated_at WHERE archived = TRUE AND archived_at IS NULL`);
}
// Vérifier si completed_at existe pour global_todos
const globalTodosCompletedAtExists = await client.query(`
SELECT column_name
FROM information_schema.columns
WHERE table_name='global_todos' AND column_name='completed_at'
`);
if (globalTodosCompletedAtExists.rows.length === 0) {
logger.info(' → Ajout du champ completed_at à global_todos');
await client.query(`ALTER TABLE global_todos ADD COLUMN completed_at TIMESTAMP`);
await client.query(`UPDATE global_todos SET completed_at = created_at WHERE completed = TRUE AND completed_at IS NULL`);
}
// Vérifier si note_todos a created_at et completed_at
const noteTodosCreatedAtExists = await client.query(`
SELECT column_name
FROM information_schema.columns
WHERE table_name='note_todos' AND column_name='created_at'
`);
if (noteTodosCreatedAtExists.rows.length === 0) {
logger.info(' → Ajout du champ created_at à note_todos');
await client.query(`ALTER TABLE note_todos ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP`);
}
const noteTodosCompletedAtExists = await client.query(`
SELECT column_name
FROM information_schema.columns
WHERE table_name='note_todos' AND column_name='completed_at'
`);
if (noteTodosCompletedAtExists.rows.length === 0) {
logger.info(' → Ajout du champ completed_at à note_todos');
await client.query(`ALTER TABLE note_todos ADD COLUMN completed_at TIMESTAMP`);
await client.query(`UPDATE note_todos SET completed_at = CURRENT_TIMESTAMP WHERE completed = TRUE AND completed_at IS NULL`);
}
// Créer les triggers pour mise à jour automatique
await client.query(`
CREATE OR REPLACE FUNCTION update_notes_archived_at()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.archived = TRUE AND OLD.archived = FALSE THEN
NEW.archived_at = CURRENT_TIMESTAMP;
ELSIF NEW.archived = FALSE THEN
NEW.archived_at = NULL;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
`);
await client.query(`
DROP TRIGGER IF EXISTS trigger_notes_archived_at ON notes;
CREATE TRIGGER trigger_notes_archived_at
BEFORE UPDATE ON notes
FOR EACH ROW
EXECUTE FUNCTION update_notes_archived_at();
`);
await client.query(`
CREATE OR REPLACE FUNCTION update_global_todos_completed_at()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.completed = TRUE AND OLD.completed = FALSE THEN
NEW.completed_at = CURRENT_TIMESTAMP;
ELSIF NEW.completed = FALSE THEN
NEW.completed_at = NULL;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
`);
await client.query(`
DROP TRIGGER IF EXISTS trigger_global_todos_completed_at ON global_todos;
CREATE TRIGGER trigger_global_todos_completed_at
BEFORE UPDATE ON global_todos
FOR EACH ROW
EXECUTE FUNCTION update_global_todos_completed_at();
`);
await client.query(`
CREATE OR REPLACE FUNCTION update_note_todos_completed_at()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.completed = TRUE AND (OLD.completed IS NULL OR OLD.completed = FALSE) THEN
NEW.completed_at = CURRENT_TIMESTAMP;
ELSIF NEW.completed = FALSE THEN
NEW.completed_at = NULL;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
`);
await client.query(`
DROP TRIGGER IF EXISTS trigger_note_todos_completed_at ON note_todos;
CREATE TRIGGER trigger_note_todos_completed_at
BEFORE UPDATE ON note_todos
FOR EACH ROW
EXECUTE FUNCTION update_note_todos_completed_at();
`);
// Migration 2: Champ priority pour les tâches
logger.info(' Vérification: champ priority pour tâches...');
const globalTodosPriorityExists = await client.query(`
SELECT column_name
FROM information_schema.columns
WHERE table_name='global_todos' AND column_name='priority'
`);
if (globalTodosPriorityExists.rows.length === 0) {
logger.info(' → Ajout du champ priority à global_todos');
await client.query(`ALTER TABLE global_todos ADD COLUMN priority BOOLEAN DEFAULT FALSE`);
}
const noteTodosPriorityExists = await client.query(`
SELECT column_name
FROM information_schema.columns
WHERE table_name='note_todos' AND column_name='priority'
`);
if (noteTodosPriorityExists.rows.length === 0) {
logger.info(' → Ajout du champ priority à note_todos');
await client.query(`ALTER TABLE note_todos ADD COLUMN priority BOOLEAN DEFAULT FALSE`);
}
// Créer les index pour performance
await client.query(`CREATE INDEX IF NOT EXISTS idx_global_todos_priority ON global_todos(priority DESC, created_at DESC)`);
await client.query(`CREATE INDEX IF NOT EXISTS idx_note_todos_priority ON note_todos(priority DESC, position)`);
await client.query('COMMIT');
logger.info('✅ Migrations automatiques terminées avec succès');
} catch (error) {
await client.query('ROLLBACK');
logger.error('❌ Erreur lors des migrations automatiques:', error);
throw error;
} finally {
client.release();
}
await pool.end();
} catch (error) {
logger.error('❌ Erreur fatale lors des migrations:', error);
// Ne pas crasher l'application, juste logger l'erreur
}
}
// Si appelé directement
if (require.main === module) {
autoMigrate().then(() => {
process.exit(0);
}).catch((error) => {
console.error(error);
process.exit(1);
});
}
module.exports = { autoMigrate };
+5
View File
@@ -134,6 +134,11 @@ async function startServer() {
await initDatabase();
logger.info('✓ Base de données initialisée avec succès');
// Exécuter les migrations automatiques
const { autoMigrate } = require('./scripts/auto-migrate');
await autoMigrate();
logger.info('✓ Migrations automatiques appliquées');
// Démarrer le scheduler RSS
const rssScheduler = require('./services/rss-scheduler');
rssScheduler.startScheduler();