mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-11 17:29:37 +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
+869
-10
No files matched your search
+5
-6
@@ -33,16 +33,14 @@ FROM node:20-alpine
|
|||||||
# Définir le répertoire de travail
|
# Définir le répertoire de travail
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Installer SQLite et les outils de build pour les modules natifs
|
# Installer PostgreSQL client et les outils de build pour les modules natifs
|
||||||
RUN apk add --no-cache sqlite python3 make g++
|
RUN apk add --no-cache postgresql-client python3 make g++
|
||||||
|
|
||||||
# Copier les fichiers de dépendances
|
# Copier les fichiers de dépendances
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
|
|
||||||
# Installer les dépendances backend
|
# Installer les dépendances backend
|
||||||
# Note: On installe toutes les dépendances car package.json contient aussi les deps frontend
|
RUN npm ci --only=production
|
||||||
# mais seules les deps backend sont utilisées en runtime (express, sqlite3, bcrypt, etc.)
|
|
||||||
RUN npm ci
|
|
||||||
|
|
||||||
# Copier le code du serveur
|
# Copier le code du serveur
|
||||||
COPY server.js ./
|
COPY server.js ./
|
||||||
@@ -51,12 +49,13 @@ COPY routes ./routes
|
|||||||
COPY middleware ./middleware
|
COPY middleware ./middleware
|
||||||
COPY services ./services
|
COPY services ./services
|
||||||
COPY migrations ./migrations
|
COPY migrations ./migrations
|
||||||
|
COPY scripts ./scripts
|
||||||
|
|
||||||
# Copier le build de l'application React depuis le builder
|
# Copier le build de l'application React depuis le builder
|
||||||
COPY --from=builder /app/dist ./dist
|
COPY --from=builder /app/dist ./dist
|
||||||
|
|
||||||
# Créer les dossiers nécessaires et définir les permissions
|
# Créer les dossiers nécessaires et définir les permissions
|
||||||
RUN mkdir -p /app/data /app/public/uploads && \
|
RUN mkdir -p /app/data/logs /app/public/uploads && \
|
||||||
chown -R node:node /app
|
chown -R node:node /app
|
||||||
|
|
||||||
# Utiliser l'utilisateur node pour la sécurité
|
# Utiliser l'utilisateur node pour la sécurité
|
||||||
|
|||||||
@@ -0,0 +1,328 @@
|
|||||||
|
# Migration vers PostgreSQL + Docker
|
||||||
|
|
||||||
|
Ce guide explique comment migrer NoteFlow de SQLite vers PostgreSQL avec Docker.
|
||||||
|
|
||||||
|
## 🎯 Pourquoi PostgreSQL?
|
||||||
|
|
||||||
|
### Problèmes avec SQLite
|
||||||
|
- ❌ Concurrence limitée (verrous sur toute la DB)
|
||||||
|
- ❌ Difficile de savoir quelle DB est utilisée
|
||||||
|
- ❌ Pas de logs de requêtes natifs
|
||||||
|
- ❌ Corruption possible en cas de crash
|
||||||
|
- ❌ Pas adapté pour la production
|
||||||
|
|
||||||
|
### Avantages de PostgreSQL
|
||||||
|
- ✅ Concurrence parfaite (MVCC)
|
||||||
|
- ✅ Une seule source de vérité
|
||||||
|
- ✅ Transactions ACID robustes
|
||||||
|
- ✅ Logs et monitoring natifs
|
||||||
|
- ✅ Production-ready et performant
|
||||||
|
- ✅ Intégration Docker parfaite
|
||||||
|
|
||||||
|
## 📋 Prérequis
|
||||||
|
|
||||||
|
- Docker et Docker Compose installés
|
||||||
|
- Sauvegarde de votre base SQLite actuelle
|
||||||
|
- 5 minutes de votre temps
|
||||||
|
|
||||||
|
## 🚀 Étapes de Migration
|
||||||
|
|
||||||
|
### 1. Installer les dépendances PostgreSQL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install pg@^8.11.3
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Activer PostgreSQL dans database.js
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backup de l'ancien fichier
|
||||||
|
cp config/database.js config/database-sqlite.js.backup
|
||||||
|
|
||||||
|
# Installer la nouvelle version PostgreSQL
|
||||||
|
cp config/database-postgres.js config/database.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Créer le fichier .env (optionnel)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat > .env <<EOF
|
||||||
|
# PostgreSQL
|
||||||
|
POSTGRES_PASSWORD=votre_mot_de_passe_securise_ici
|
||||||
|
DATABASE_URL=postgresql://noteflow:votre_mot_de_passe_securise_ici@localhost:5499/noteflow
|
||||||
|
|
||||||
|
# JWT
|
||||||
|
JWT_SECRET=votre_secret_jwt_ici
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Démarrer PostgreSQL avec Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Démarrer uniquement PostgreSQL
|
||||||
|
docker-compose up -d postgres
|
||||||
|
|
||||||
|
# Vérifier que PostgreSQL est démarré
|
||||||
|
docker-compose logs postgres
|
||||||
|
|
||||||
|
# Vous devriez voir: "database system is ready to accept connections"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Migrer les données SQLite → PostgreSQL
|
||||||
|
|
||||||
|
**IMPORTANT:** Localisez d'abord votre vraie base SQLite
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Chercher où est votre vraie base de données
|
||||||
|
find / -name "notes.db" 2>/dev/null
|
||||||
|
|
||||||
|
# Ou regarder dans les logs du serveur
|
||||||
|
grep "Base de données connectée" data/app.log
|
||||||
|
```
|
||||||
|
|
||||||
|
Une fois trouvée, lancez la migration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Exemple si la DB est dans /app/data/notes.db
|
||||||
|
SQLITE_PATH=/app/data/notes.db node scripts/migrate-sqlite-to-postgres.js
|
||||||
|
|
||||||
|
# Ou si locale
|
||||||
|
SQLITE_PATH=./data/notes.db node scripts/migrate-sqlite-to-postgres.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Vous devriez voir:
|
||||||
|
|
||||||
|
```
|
||||||
|
==================== MIGRATION SQLite → PostgreSQL ====================
|
||||||
|
|
||||||
|
✓ SQLite connecté: /app/data/notes.db
|
||||||
|
✓ PostgreSQL connecté
|
||||||
|
|
||||||
|
📊 Début de la migration...
|
||||||
|
|
||||||
|
📋 Migration table: users
|
||||||
|
✓ Table "users": 2/2 lignes migrées
|
||||||
|
📋 Migration table: notes
|
||||||
|
✓ Table "notes": 45/45 lignes migrées
|
||||||
|
📋 Migration table: rss_articles
|
||||||
|
✓ Table "rss_articles": 127/127 lignes migrées
|
||||||
|
...
|
||||||
|
|
||||||
|
========================================================
|
||||||
|
✅ Migration terminée!
|
||||||
|
Total: 250 lignes migrées
|
||||||
|
========================================================
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Redémarrer l'application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Reconstruire l'image Docker
|
||||||
|
docker-compose build notes-app
|
||||||
|
|
||||||
|
# Redémarrer tout
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Voir les logs
|
||||||
|
docker-compose logs -f notes-app
|
||||||
|
```
|
||||||
|
|
||||||
|
Vous devriez voir:
|
||||||
|
|
||||||
|
```
|
||||||
|
[INFO]: ✓ PostgreSQL connecté: postgresql://noteflow:***@postgres:5499/noteflow
|
||||||
|
[INFO]: ✓ Tables PostgreSQL créées avec succès
|
||||||
|
[INFO]: Server running on port 2222
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ Vérification
|
||||||
|
|
||||||
|
### 1. Tester la connexion PostgreSQL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Se connecter à PostgreSQL
|
||||||
|
docker exec -it noteflow-postgres psql -U noteflow -d noteflow
|
||||||
|
|
||||||
|
# Vérifier les tables
|
||||||
|
\dt
|
||||||
|
|
||||||
|
# Compter les articles RSS
|
||||||
|
SELECT COUNT(*) FROM rss_articles;
|
||||||
|
|
||||||
|
# Voir les 5 derniers articles
|
||||||
|
SELECT title, pub_date FROM rss_articles ORDER BY pub_date DESC LIMIT 5;
|
||||||
|
|
||||||
|
# Quitter
|
||||||
|
\q
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Tester l'application
|
||||||
|
|
||||||
|
1. Ouvrez http://localhost:2222
|
||||||
|
2. Connectez-vous (admin/admin)
|
||||||
|
3. Vérifiez que vos notes sont là
|
||||||
|
4. Vérifiez que les flux RSS s'affichent
|
||||||
|
5. Ajoutez un nouveau flux RSS
|
||||||
|
6. Attendez 2 minutes et vérifiez les nouveaux articles
|
||||||
|
|
||||||
|
## 🔧 Configuration
|
||||||
|
|
||||||
|
### Variables d'environnement
|
||||||
|
|
||||||
|
Le système utilise `DATABASE_URL` pour se connecter à PostgreSQL:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DATABASE_URL=postgresql://USER:PASSWORD@HOST:PORT/DATABASE
|
||||||
|
```
|
||||||
|
|
||||||
|
Exemple:
|
||||||
|
```bash
|
||||||
|
DATABASE_URL=postgresql://noteflow:mon_password@postgres:5432/noteflow
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dans docker-compose.yml
|
||||||
|
|
||||||
|
PostgreSQL tourne sur le **port 5499** (externe) et communique sur le **port 5432** (interne):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
postgres:
|
||||||
|
ports:
|
||||||
|
- "5499:5499" # Accessible depuis l'hôte sur 5499
|
||||||
|
```
|
||||||
|
|
||||||
|
L'application se connecte via le nom du service:
|
||||||
|
```yaml
|
||||||
|
notes-app:
|
||||||
|
environment:
|
||||||
|
- DATABASE_URL=postgresql://noteflow:password@postgres:5432/noteflow
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Maintenance PostgreSQL
|
||||||
|
|
||||||
|
### Backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backup complet
|
||||||
|
docker exec noteflow-postgres pg_dump -U noteflow -d noteflow > backup.sql
|
||||||
|
|
||||||
|
# Backup compressé
|
||||||
|
docker exec noteflow-postgres pg_dump -U noteflow -d noteflow | gzip > backup.sql.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restore
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Depuis un backup
|
||||||
|
docker exec -i noteflow-postgres psql -U noteflow -d noteflow < backup.sql
|
||||||
|
|
||||||
|
# Depuis un backup compressé
|
||||||
|
gunzip < backup.sql.gz | docker exec -i noteflow-postgres psql -U noteflow -d noteflow
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Voir les logs PostgreSQL
|
||||||
|
docker-compose logs postgres
|
||||||
|
|
||||||
|
# Logs en temps réel
|
||||||
|
docker-compose logs -f postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Se connecter à PostgreSQL
|
||||||
|
docker exec -it noteflow-postgres psql -U noteflow -d noteflow
|
||||||
|
|
||||||
|
# Voir les requêtes lentes
|
||||||
|
SELECT pid, now() - query_start AS duration, query
|
||||||
|
FROM pg_stat_activity
|
||||||
|
WHERE state = 'active'
|
||||||
|
ORDER BY duration DESC;
|
||||||
|
|
||||||
|
# Taille de la base
|
||||||
|
SELECT pg_size_pretty(pg_database_size('noteflow'));
|
||||||
|
|
||||||
|
# Taille des tables
|
||||||
|
SELECT schemaname, tablename, pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
|
||||||
|
FROM pg_tables
|
||||||
|
WHERE schemaname = 'public'
|
||||||
|
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐛 Dépannage
|
||||||
|
|
||||||
|
### Problème: PostgreSQL ne démarre pas
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Vérifier les logs
|
||||||
|
docker-compose logs postgres
|
||||||
|
|
||||||
|
# Supprimer le volume et recommencer
|
||||||
|
docker-compose down -v
|
||||||
|
docker-compose up -d postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problème: Application ne se connecte pas
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Vérifier les variables d'environnement
|
||||||
|
docker exec notes-todo-app env | grep DATABASE
|
||||||
|
|
||||||
|
# Vérifier que PostgreSQL est accessible
|
||||||
|
docker exec notes-todo-app nc -zv postgres 5432
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problème: Articles RSS toujours pas affichés
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Se connecter à PostgreSQL
|
||||||
|
docker exec -it noteflow-postgres psql -U noteflow -d noteflow
|
||||||
|
|
||||||
|
# Compter les articles
|
||||||
|
SELECT COUNT(*) FROM rss_articles;
|
||||||
|
|
||||||
|
# Voir les 10 derniers
|
||||||
|
SELECT id, title, pub_date FROM rss_articles ORDER BY pub_date DESC LIMIT 10;
|
||||||
|
|
||||||
|
# Vérifier les flux
|
||||||
|
SELECT id, title, enabled FROM rss_feeds;
|
||||||
|
```
|
||||||
|
|
||||||
|
Si la table est vide, le problème vient du scheduler. Vérifiez les logs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose logs -f notes-app | grep RSS
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 Rollback vers SQLite
|
||||||
|
|
||||||
|
Si vous devez revenir à SQLite:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Arrêter Docker
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# 2. Restaurer l'ancien database.js
|
||||||
|
cp config/database-sqlite.js.backup config/database.js
|
||||||
|
|
||||||
|
# 3. Modifier docker-compose.yml
|
||||||
|
# Commentez la section postgres et remettez DB_PATH
|
||||||
|
|
||||||
|
# 4. Redémarrer
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✨ Résultat Attendu
|
||||||
|
|
||||||
|
Après migration vers PostgreSQL:
|
||||||
|
|
||||||
|
✅ **Problème RSS résolu**: Les nouveaux articles s'affichent immédiatement
|
||||||
|
✅ **Pas de confusion DB**: Une seule source de vérité
|
||||||
|
✅ **Debuggable**: Logs SQL natifs dans PostgreSQL
|
||||||
|
✅ **Performance**: Meilleure concurrence et vitesse
|
||||||
|
✅ **Production-ready**: Système robuste et fiable
|
||||||
|
✅ **Monitoring**: Outils natifs PostgreSQL disponibles
|
||||||
|
|
||||||
|
**Les articles du 14 novembre (et suivants) seront visibles dès le prochain cycle du scheduler !** 🎉
|
||||||
@@ -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
|
||||||
|
};
|
||||||
+34
-4
@@ -1,21 +1,50 @@
|
|||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
# PostgreSQL Database
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: noteflow-postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
command: -p 5499
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: noteflow
|
||||||
|
POSTGRES_USER: noteflow
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-noteflow_secure_password_change_me}
|
||||||
|
POSTGRES_INITDB_ARGS: "--encoding=UTF8"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- "5499:5499"
|
||||||
|
networks:
|
||||||
|
- nginx_default
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U noteflow -d noteflow -p 5499"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# NoteFlow Application
|
||||||
notes-app:
|
notes-app:
|
||||||
build: .
|
build: .
|
||||||
container_name: notes-todo-app
|
container_name: notes-todo-app
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
ports:
|
ports:
|
||||||
- "2222:2222"
|
- "2222:2222"
|
||||||
volumes:
|
volumes:
|
||||||
- notes_data:/app/data
|
|
||||||
- notes_uploads:/app/public/uploads
|
- notes_uploads:/app/public/uploads
|
||||||
|
- notes_logs:/app/data/logs
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
- JWT_SECRET=${JWT_SECRET:-change_me_in_production_please_use_strong_secret}
|
- JWT_SECRET=${JWT_SECRET:-change_me_in_production_please_use_strong_secret}
|
||||||
- PORT=2222
|
- PORT=2222
|
||||||
- UPLOADS_PATH=/app/public/uploads
|
- UPLOADS_PATH=/app/public/uploads
|
||||||
- DB_PATH=/app/data/notes.db
|
# PostgreSQL connection
|
||||||
restart: unless-stopped
|
- DATABASE_URL=postgresql://noteflow:${POSTGRES_PASSWORD:-noteflow_secure_password_change_me}@postgres:5499/noteflow
|
||||||
|
- DB_TYPE=postgres
|
||||||
networks:
|
networks:
|
||||||
- nginx_default
|
- nginx_default
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@@ -26,8 +55,9 @@ services:
|
|||||||
start_period: 40s
|
start_period: 40s
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
notes_data:
|
postgres_data:
|
||||||
notes_uploads:
|
notes_uploads:
|
||||||
|
notes_logs:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
nginx_default:
|
nginx_default:
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- Script d'initialisation PostgreSQL pour NoteFlow
|
||||||
|
-- Exécuté automatiquement au premier démarrage du conteneur
|
||||||
|
|
||||||
|
-- Extensions utiles
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||||
|
|
||||||
|
-- Créer les index après création des tables dans database.js
|
||||||
|
-- Ce script peut rester vide si database.js gère tout
|
||||||
@@ -76,6 +76,7 @@
|
|||||||
"lucide-react": "^0.469.0",
|
"lucide-react": "^0.469.0",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"next-themes": "^0.4.4",
|
"next-themes": "^0.4.4",
|
||||||
|
"pg": "^8.11.3",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-day-picker": "^9.11.1",
|
"react-day-picker": "^9.11.1",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
// Script de migration SQLite → PostgreSQL
|
||||||
|
const sqlite3 = require('sqlite3').verbose();
|
||||||
|
const { Pool } = require('pg');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
console.log('\n==================== MIGRATION SQLite → PostgreSQL ====================\n');
|
||||||
|
|
||||||
|
const SQLITE_PATH = process.env.SQLITE_PATH || path.join(__dirname, '../data/notes.db');
|
||||||
|
const DATABASE_URL = process.env.DATABASE_URL ||
|
||||||
|
`postgresql://noteflow:noteflow_secure_password_change_me@localhost:5499/noteflow`;
|
||||||
|
|
||||||
|
// Connexion SQLite (source)
|
||||||
|
const sqliteDb = new sqlite3.Database(SQLITE_PATH, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Erreur connexion SQLite:', err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log('✓ SQLite connecté:', SQLITE_PATH);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connexion PostgreSQL (destination)
|
||||||
|
const pgPool = new Pool({
|
||||||
|
connectionString: DATABASE_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
pgPool.connect((err, client, release) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Erreur connexion PostgreSQL:', err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
release();
|
||||||
|
console.log('✓ PostgreSQL connecté\n');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tables à migrer (dans l'ordre pour respecter les foreign keys)
|
||||||
|
const TABLES_ORDER = [
|
||||||
|
'users',
|
||||||
|
'notes',
|
||||||
|
'note_todos',
|
||||||
|
'global_todos',
|
||||||
|
'note_images',
|
||||||
|
'note_files',
|
||||||
|
'settings',
|
||||||
|
'rss_feeds',
|
||||||
|
'rss_articles',
|
||||||
|
'rss_summaries',
|
||||||
|
'calendar_events',
|
||||||
|
'note_tags',
|
||||||
|
'google_oauth_tokens'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupérer les données d'une table SQLite
|
||||||
|
*/
|
||||||
|
function getSqliteData(tableName) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
sqliteDb.all(`SELECT * FROM ${tableName}`, (err, rows) => {
|
||||||
|
if (err) {
|
||||||
|
if (err.message.includes('no such table')) {
|
||||||
|
resolve([]); // Table n'existe pas, ok
|
||||||
|
} else {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolve(rows);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insérer les données dans PostgreSQL
|
||||||
|
*/
|
||||||
|
async function insertPostgresData(tableName, rows) {
|
||||||
|
if (rows.length === 0) {
|
||||||
|
console.log(` ⏭️ Table "${tableName}" vide, skip`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = await pgPool.connect();
|
||||||
|
let inserted = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.query('BEGIN');
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
// Convertir les noms de colonnes et valeurs
|
||||||
|
const columns = Object.keys(row);
|
||||||
|
const values = Object.values(row).map(val => {
|
||||||
|
// Convertir les valeurs SQLite vers PostgreSQL
|
||||||
|
if (val === null) return null;
|
||||||
|
if (typeof val === 'boolean') return val;
|
||||||
|
if (typeof val === 'number') return val;
|
||||||
|
// Dates
|
||||||
|
if (typeof val === 'string' && /^\d{4}-\d{2}-\d{2}/.test(val)) {
|
||||||
|
return val; // Déjà au bon format
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
});
|
||||||
|
|
||||||
|
const placeholders = values.map((_, i) => `$${i + 1}`).join(', ');
|
||||||
|
const columnsStr = columns.join(', ');
|
||||||
|
|
||||||
|
const sql = `
|
||||||
|
INSERT INTO ${tableName} (${columnsStr})
|
||||||
|
VALUES (${placeholders})
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.query(sql, values);
|
||||||
|
inserted++;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(` ⚠️ Erreur insertion ligne (ignorée):`, err.message.substring(0, 100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset des séquences pour les colonnes SERIAL
|
||||||
|
try {
|
||||||
|
await client.query(`
|
||||||
|
SELECT setval(pg_get_serial_sequence('${tableName}', 'id'),
|
||||||
|
(SELECT MAX(id) FROM ${tableName}))
|
||||||
|
`);
|
||||||
|
} catch (err) {
|
||||||
|
// Ignore si pas de colonne id
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.query('COMMIT');
|
||||||
|
console.log(` ✓ Table "${tableName}": ${inserted}/${rows.length} lignes migrées`);
|
||||||
|
return inserted;
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
await client.query('ROLLBACK');
|
||||||
|
console.error(` ✗ Erreur table "${tableName}":`, err.message);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migration principale
|
||||||
|
*/
|
||||||
|
async function migrate() {
|
||||||
|
let totalRows = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('📊 Début de la migration...\n');
|
||||||
|
|
||||||
|
for (const table of TABLES_ORDER) {
|
||||||
|
console.log(`📋 Migration table: ${table}`);
|
||||||
|
|
||||||
|
const rows = await getSqliteData(table);
|
||||||
|
const inserted = await insertPostgresData(table, rows);
|
||||||
|
totalRows += inserted;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n========================================================');
|
||||||
|
console.log(`✅ Migration terminée!`);
|
||||||
|
console.log(`Total: ${totalRows} lignes migrées`);
|
||||||
|
console.log('========================================================\n');
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('\n❌ Erreur lors de la migration:', err);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
sqliteDb.close();
|
||||||
|
await pgPool.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que la DB SQLite existe
|
||||||
|
if (!fs.existsSync(SQLITE_PATH)) {
|
||||||
|
console.error(`❌ Fichier SQLite introuvable: ${SQLITE_PATH}`);
|
||||||
|
console.log('\nUtilisation:');
|
||||||
|
console.log(' SQLITE_PATH=/path/to/notes.db node scripts/migrate-sqlite-to-postgres.js');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lancer la migration
|
||||||
|
migrate();
|
||||||
Reference in new issue
Block a user