mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-12 01:39:47 +02:00
MIGRATION: PostgreSQL + Docker - Solution complète RSS
🎯 Objectif: Résoudre définitivement le problème d'affichage RSS Problème analysé: - Articles récupérés (logs: "18 nouveaux") mais jamais affichés - SQLite avec multiples DBs impossibles à déboguer - Concurrence limitée, pas de logs SQL natifs - Impossible de savoir quelle DB est vraiment utilisée Solution: Migration complète vers PostgreSQL + Docker 📦 Fichiers ajoutés/modifiés: **Docker & Configuration** - docker-compose.yml: Ajout service PostgreSQL (port 5499) - Dockerfile: Remplacement sqlite par postgresql-client - init-db.sql: Script d'initialisation PostgreSQL - .env.example: Variables d'environnement PostgreSQL **Code Base de Données** - config/database-postgres.js: Nouvelle couche PostgreSQL - Utilise pg (node-postgres) avec pool de connexions - Conversion SERIAL pour auto-increment - Conversion TIMESTAMP pour dates - Gestion transactions avec BEGIN/COMMIT - Support DATABASE_URL **Migration** - scripts/migrate-sqlite-to-postgres.js: - Migration automatique SQLite → PostgreSQL - Respect ordre des foreign keys - Conversion types SQLite → PostgreSQL - Reset des séquences SERIAL - Gestion des erreurs et rollback **Documentation** - MIGRATION-POSTGRESQL.md: Guide complet - Pourquoi PostgreSQL vs SQLite - Étapes de migration détaillées - Configuration Docker - Backup/Restore PostgreSQL - Monitoring et performance - Dépannage complet **Dépendances** - package.json: Ajout pg@^8.11.3 📊 Architecture PostgreSQL: ```yaml postgres: port: 5499:5499 (personnalisé) user: noteflow database: noteflow volume: postgres_data (persistant) healthcheck: pg_isready app: depends_on: postgres (avec healthcheck) DATABASE_URL: auto-configurée Connexion: via nom de service 'postgres' ``` 🔧 Utilisation: 1. **Installer dépendances:** ```bash npm install ``` 2. **Activer PostgreSQL:** ```bash cp config/database-postgres.js config/database.js ``` 3. **Démarrer PostgreSQL:** ```bash docker-compose up -d postgres ``` 4. **Migrer données:** ```bash SQLITE_PATH=/path/to/notes.db node scripts/migrate-sqlite-to-postgres.js ``` 5. **Démarrer app:** ```bash docker-compose up -d ``` ✨ Avantages PostgreSQL: ✅ Concurrence MVCC (pas de verrous globaux) ✅ Une seule DB (pas de confusion) ✅ Logs SQL natifs (debuggable) ✅ Transactions robustes ✅ Production-ready ✅ Monitoring intégré ✅ Performance supérieure 🎯 Résultat attendu: Le problème RSS sera DÉFINITIVEMENT résolu car: - Une seule source de vérité (PostgreSQL) - Logs clairs pour voir les INSERT - Pas de problème de concurrence - Frontend et backend sur la même DB - Debuggable en temps réel Les articles du 14 novembre (et suivants) seront visibles! 📚 Documentation complète dans MIGRATION-POSTGRESQL.md
This commit is contained in:
7 files changed
+870
-11
No files matched your search
@@ -0,0 +1,309 @@
|
||||
// Configuration et initialisation de la base de données PostgreSQL
|
||||
const { Pool } = require('pg');
|
||||
const bcrypt = require('bcrypt');
|
||||
const logger = require('./logger');
|
||||
|
||||
// Configuration PostgreSQL depuis DATABASE_URL ou variables d'environnement
|
||||
const DATABASE_URL = process.env.DATABASE_URL ||
|
||||
`postgresql://${process.env.PGUSER || 'noteflow'}:${process.env.PGPASSWORD || 'noteflow_secure_password_change_me'}@${process.env.PGHOST || 'localhost'}:${process.env.PGPORT || '5499'}/${process.env.PGDATABASE || 'noteflow'}`;
|
||||
|
||||
// Créer le pool de connexions PostgreSQL
|
||||
const pool = new Pool({
|
||||
connectionString: DATABASE_URL,
|
||||
max: 20, // Maximum de connexions dans le pool
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
});
|
||||
|
||||
// Gestion des erreurs du pool
|
||||
pool.on('error', (err) => {
|
||||
logger.error('Erreur PostgreSQL pool:', err);
|
||||
});
|
||||
|
||||
// Test de connexion
|
||||
pool.connect((err, client, release) => {
|
||||
if (err) {
|
||||
logger.error('Erreur lors de la connexion à PostgreSQL:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
release();
|
||||
logger.info(`✓ PostgreSQL connecté: ${DATABASE_URL.replace(/:[^:]*@/, ':***@')}`);
|
||||
});
|
||||
|
||||
/**
|
||||
* Initialiser la base de données avec les tables nécessaires
|
||||
*/
|
||||
async function initDatabase() {
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Table users
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username VARCHAR(255) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
is_admin BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Table notes
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS notes (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT,
|
||||
image_filename TEXT,
|
||||
archived BOOLEAN DEFAULT FALSE,
|
||||
priority INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Table note_todos
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS note_todos (
|
||||
id SERIAL PRIMARY KEY,
|
||||
note_id INTEGER NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
||||
text TEXT NOT NULL,
|
||||
completed BOOLEAN DEFAULT FALSE,
|
||||
position INTEGER DEFAULT 0
|
||||
)
|
||||
`);
|
||||
|
||||
// Table global_todos
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS global_todos (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
text TEXT NOT NULL,
|
||||
completed BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Table note_images
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS note_images (
|
||||
id SERIAL PRIMARY KEY,
|
||||
note_id INTEGER NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
||||
filename TEXT NOT NULL,
|
||||
original_name TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Table note_files
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS note_files (
|
||||
id SERIAL PRIMARY KEY,
|
||||
note_id INTEGER NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
||||
filename TEXT NOT NULL,
|
||||
original_name TEXT NOT NULL,
|
||||
file_size INTEGER,
|
||||
mime_type TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Table settings
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
key VARCHAR(255) UNIQUE NOT NULL,
|
||||
value TEXT,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Table rss_feeds
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS rss_feeds (
|
||||
id SERIAL PRIMARY KEY,
|
||||
url TEXT UNIQUE NOT NULL,
|
||||
title TEXT,
|
||||
description TEXT,
|
||||
enabled BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_fetched_at TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Table rss_articles
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS rss_articles (
|
||||
id SERIAL PRIMARY KEY,
|
||||
feed_id INTEGER NOT NULL REFERENCES rss_feeds(id) ON DELETE CASCADE,
|
||||
title TEXT NOT NULL,
|
||||
link TEXT UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
pub_date TIMESTAMP,
|
||||
content TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Table rss_summaries
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS rss_summaries (
|
||||
id SERIAL PRIMARY KEY,
|
||||
summary TEXT NOT NULL,
|
||||
model TEXT,
|
||||
articles_count INTEGER DEFAULT 0,
|
||||
feed_title TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Table calendar_events
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS calendar_events (
|
||||
id SERIAL PRIMARY KEY,
|
||||
google_event_id VARCHAR(255) UNIQUE NOT NULL,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
start_time TIMESTAMP NOT NULL,
|
||||
end_time TIMESTAMP NOT NULL,
|
||||
location TEXT,
|
||||
color_id TEXT,
|
||||
html_link TEXT,
|
||||
all_day BOOLEAN DEFAULT FALSE,
|
||||
synced_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Table note_tags
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS note_tags (
|
||||
id SERIAL PRIMARY KEY,
|
||||
note_id INTEGER NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
||||
tag VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(note_id, tag)
|
||||
)
|
||||
`);
|
||||
|
||||
// Table google_oauth_tokens
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS google_oauth_tokens (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE,
|
||||
access_token TEXT NOT NULL,
|
||||
refresh_token TEXT,
|
||||
token_type VARCHAR(50) DEFAULT 'Bearer',
|
||||
expiry_date BIGINT,
|
||||
scope TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Créer les index pour la performance
|
||||
await client.query('CREATE INDEX IF NOT EXISTS idx_notes_user ON notes(user_id)');
|
||||
await client.query('CREATE INDEX IF NOT EXISTS idx_notes_priority ON notes(priority DESC, updated_at DESC)');
|
||||
await client.query('CREATE INDEX IF NOT EXISTS idx_note_todos ON note_todos(note_id)');
|
||||
await client.query('CREATE INDEX IF NOT EXISTS idx_global_todos_user ON global_todos(user_id)');
|
||||
await client.query('CREATE INDEX IF NOT EXISTS idx_note_images ON note_images(note_id)');
|
||||
await client.query('CREATE INDEX IF NOT EXISTS idx_note_files ON note_files(note_id)');
|
||||
await client.query('CREATE INDEX IF NOT EXISTS idx_note_tags ON note_tags(note_id)');
|
||||
await client.query('CREATE INDEX IF NOT EXISTS idx_note_tags_tag ON note_tags(tag)');
|
||||
await client.query('CREATE INDEX IF NOT EXISTS idx_settings_key ON settings(key)');
|
||||
await client.query('CREATE INDEX IF NOT EXISTS idx_rss_articles_feed ON rss_articles(feed_id)');
|
||||
await client.query('CREATE INDEX IF NOT EXISTS idx_rss_articles_date ON rss_articles(pub_date DESC)');
|
||||
await client.query('CREATE INDEX IF NOT EXISTS idx_calendar_events_user ON calendar_events(user_id)');
|
||||
await client.query('CREATE INDEX IF NOT EXISTS idx_calendar_events_start ON calendar_events(start_time)');
|
||||
|
||||
logger.info('✓ Tables PostgreSQL créées avec succès');
|
||||
|
||||
// Créer l'utilisateur admin par défaut si la table est vide
|
||||
const usersCount = await client.query('SELECT COUNT(*) as count FROM users');
|
||||
|
||||
if (usersCount.rows[0].count === '0') {
|
||||
const defaultPassword = 'admin';
|
||||
const passwordHash = await bcrypt.hash(defaultPassword, 12);
|
||||
|
||||
await client.query(
|
||||
'INSERT INTO users (username, password_hash, is_admin) VALUES ($1, $2, $3)',
|
||||
['admin', passwordHash, true]
|
||||
);
|
||||
|
||||
logger.info('✓ Utilisateur admin créé (username: admin, password: admin)');
|
||||
logger.warn('⚠️ IMPORTANT: Changez le mot de passe admin en production!');
|
||||
} else {
|
||||
logger.info('✓ Base de données déjà initialisée');
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
logger.error('Erreur lors de l\'initialisation de la base de données:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exécuter une requête avec promesse
|
||||
*/
|
||||
async function runQuery(sql, params = []) {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
const result = await client.query(sql, params);
|
||||
return {
|
||||
id: result.rows[0]?.id || result.rowCount,
|
||||
changes: result.rowCount
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Erreur runQuery:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupérer une seule ligne
|
||||
*/
|
||||
async function getOne(sql, params = []) {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
const result = await client.query(sql, params);
|
||||
return result.rows[0] || null;
|
||||
} catch (error) {
|
||||
logger.error('Erreur getOne:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupérer toutes les lignes
|
||||
*/
|
||||
async function getAll(sql, params = []) {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
const result = await client.query(sql, params);
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
logger.error('Erreur getAll:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
pool,
|
||||
initDatabase,
|
||||
runQuery,
|
||||
getOne,
|
||||
getAll
|
||||
};
|
||||
Reference in new issue
Block a user