mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-23 15:08:22 +02:00
Allow users to download the database schema report
Add a new endpoint to download the database schema, and include a download button on the debug page. Replit-Commit-Author: Agent Replit-Commit-Session-Id: b163d4c0-de5e-4f4e-a9c0-aed4c7049718 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/b163d4c0-de5e-4f4e-a9c0-aed4c7049718/i9LQiyj
This commit is contained in:
1 parent
0949386425
commit
5fe9763763
2 files changed
+178
-22
No files matched your search
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Database, Eye, Loader2 } from "lucide-react";
|
import { Database, Eye, Loader2, Download } from "lucide-react";
|
||||||
import { useAuthUnified } from "@/hooks/useAuthUnified";
|
import { useAuthUnified } from "@/hooks/useAuthUnified";
|
||||||
|
|
||||||
export default function DatabaseDebug() {
|
export default function DatabaseDebug() {
|
||||||
@@ -82,23 +82,36 @@ export default function DatabaseDebug() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<div className="flex gap-2">
|
||||||
onClick={handleLogSchema}
|
<Button
|
||||||
disabled={isLoading}
|
onClick={handleLogSchema}
|
||||||
className="w-full"
|
disabled={isLoading}
|
||||||
>
|
className="flex-1"
|
||||||
{isLoading ? (
|
>
|
||||||
<>
|
{isLoading ? (
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<>
|
||||||
Scan en cours...
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
</>
|
Scan en cours...
|
||||||
) : (
|
</>
|
||||||
<>
|
) : (
|
||||||
<Database className="mr-2 h-4 w-4" />
|
<>
|
||||||
Démarrer le Scan du Schéma
|
<Database className="mr-2 h-4 w-4" />
|
||||||
</>
|
Démarrer le Scan du Schéma
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{result && (
|
||||||
|
<Button
|
||||||
|
onClick={() => window.open('/api/debug/download-schema', '_blank')}
|
||||||
|
variant="outline"
|
||||||
|
className="flex-shrink-0"
|
||||||
|
>
|
||||||
|
<Download className="mr-2 h-4 w-4" />
|
||||||
|
Télécharger
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||||
@@ -118,6 +131,9 @@ export default function DatabaseDebug() {
|
|||||||
<p className="mt-2 font-medium">
|
<p className="mt-2 font-medium">
|
||||||
📋 Consultez maintenant les logs de votre serveur pour voir le détail complet du schéma.
|
📋 Consultez maintenant les logs de votre serveur pour voir le détail complet du schéma.
|
||||||
</p>
|
</p>
|
||||||
|
<p className="mt-1 text-blue-700 font-medium">
|
||||||
|
💾 Cliquez sur "Télécharger" pour obtenir le rapport complet au format fichier texte.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+145
-5
@@ -1448,7 +1448,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
const columnsResult = await pool.query(columnsQuery, [tableName]);
|
const columnsResult = await pool.query(columnsQuery, [tableName]);
|
||||||
|
|
||||||
if (columnsResult.rows.length > 0) {
|
if (columnsResult.rows.length > 0) {
|
||||||
columnsResult.rows.forEach((col, index) => {
|
columnsResult.rows.forEach((col: any, index: number) => {
|
||||||
const nullable = col.is_nullable === 'YES' ? 'NULL' : 'NOT NULL';
|
const nullable = col.is_nullable === 'YES' ? 'NULL' : 'NOT NULL';
|
||||||
const maxLength = col.character_maximum_length ? `(${col.character_maximum_length})` : '';
|
const maxLength = col.character_maximum_length ? `(${col.character_maximum_length})` : '';
|
||||||
const defaultVal = col.column_default ? ` DEFAULT ${col.column_default}` : '';
|
const defaultVal = col.column_default ? ` DEFAULT ${col.column_default}` : '';
|
||||||
@@ -1462,7 +1462,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
const countResult = await pool.query(countQuery);
|
const countResult = await pool.query(countQuery);
|
||||||
console.log(` 📈 Nombre d'enregistrements: ${countResult.rows[0].total}`);
|
console.log(` 📈 Nombre d'enregistrements: ${countResult.rows[0].total}`);
|
||||||
} catch (countError) {
|
} catch (countError) {
|
||||||
console.log(` ⚠️ Impossible de compter les enregistrements: ${countError.message}`);
|
console.log(` ⚠️ Impossible de compter les enregistrements: ${(countError as Error).message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1493,7 +1493,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
const fkResult = await pool.query(fkQuery);
|
const fkResult = await pool.query(fkQuery);
|
||||||
|
|
||||||
if (fkResult.rows.length > 0) {
|
if (fkResult.rows.length > 0) {
|
||||||
fkResult.rows.forEach(fk => {
|
fkResult.rows.forEach((fk: any) => {
|
||||||
console.log(` ${fk.table_name}.${fk.column_name} → ${fk.foreign_table_name}.${fk.foreign_column_name}`);
|
console.log(` ${fk.table_name}.${fk.column_name} → ${fk.foreign_table_name}.${fk.foreign_column_name}`);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -1507,12 +1507,152 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
message: 'Schéma loggé avec succès dans les logs du serveur',
|
message: 'Schéma loggé avec succès dans les logs du serveur',
|
||||||
totalTables: tablesResult.rows.length,
|
totalTables: tablesResult.rows.length,
|
||||||
totalForeignKeys: fkResult.rows.length,
|
totalForeignKeys: fkResult.rows.length,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString(),
|
||||||
|
downloadUrl: '/api/debug/download-schema'
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ ERREUR lors du scan du schéma:', error);
|
console.error('❌ ERREUR lors du scan du schéma:', error);
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: (error as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Download database schema report
|
||||||
|
app.get('/api/debug/download-schema', isAuthenticated, async (req: any, res) => {
|
||||||
|
try {
|
||||||
|
const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id);
|
||||||
|
if (!user || user.role !== 'admin') {
|
||||||
|
return res.status(403).json({ message: 'Accès refusé. Seuls les administrateurs peuvent télécharger le rapport de schéma.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const isProduction = process.env.NODE_ENV === 'production';
|
||||||
|
|
||||||
|
if (!isProduction) {
|
||||||
|
return res.status(400).json({ error: 'Cette route fonctionne uniquement en production sur votre serveur privé' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que pool est disponible
|
||||||
|
if (!pool) {
|
||||||
|
throw new Error('Pool de base de données non disponible en production');
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
let reportContent = `LOGIFLOW - RAPPORT SCHÉMA BASE DE DONNÉES PRODUCTION
|
||||||
|
===============================================
|
||||||
|
|
||||||
|
GÉNÉRÉ LE : ${timestamp}
|
||||||
|
SERVEUR : Production privé LogiFlow
|
||||||
|
ENVIRONNEMENT : PostgreSQL Docker
|
||||||
|
UTILISATEUR : ${user.username}
|
||||||
|
|
||||||
|
===============================================
|
||||||
|
RÉSUMÉ DU SCAN
|
||||||
|
===============================================
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Récupérer les tables
|
||||||
|
const tablesQuery = `
|
||||||
|
SELECT table_name
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
ORDER BY table_name
|
||||||
|
`;
|
||||||
|
|
||||||
|
const tablesResult = await pool.query(tablesQuery);
|
||||||
|
|
||||||
|
reportContent += `TOTAL DES TABLES TROUVÉES: ${tablesResult.rows.length}\n`;
|
||||||
|
reportContent += `==========================================\n\n`;
|
||||||
|
|
||||||
|
// Pour chaque table, récupérer les colonnes
|
||||||
|
for (const tableRow of tablesResult.rows) {
|
||||||
|
const tableName = tableRow.table_name;
|
||||||
|
|
||||||
|
reportContent += `TABLE: ${tableName.toUpperCase()}\n`;
|
||||||
|
reportContent += `${'='.repeat(tableName.length + 6)}\n\n`;
|
||||||
|
|
||||||
|
const columnsQuery = `
|
||||||
|
SELECT
|
||||||
|
column_name,
|
||||||
|
data_type,
|
||||||
|
character_maximum_length,
|
||||||
|
is_nullable,
|
||||||
|
column_default,
|
||||||
|
ordinal_position
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name = $1
|
||||||
|
ORDER BY ordinal_position
|
||||||
|
`;
|
||||||
|
|
||||||
|
const columnsResult = await pool.query(columnsQuery, [tableName]);
|
||||||
|
|
||||||
|
if (columnsResult.rows.length > 0) {
|
||||||
|
columnsResult.rows.forEach((col: any, index: number) => {
|
||||||
|
const nullable = col.is_nullable === 'YES' ? 'NULL' : 'NOT NULL';
|
||||||
|
const maxLength = col.character_maximum_length ? `(${col.character_maximum_length})` : '';
|
||||||
|
const defaultVal = col.column_default ? ` DEFAULT ${col.column_default}` : '';
|
||||||
|
|
||||||
|
reportContent += ` ${(index + 1).toString().padStart(2, '0')}. ${col.column_name.padEnd(25)} : ${col.data_type}${maxLength} ${nullable}${defaultVal}\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Compter les enregistrements dans la table
|
||||||
|
try {
|
||||||
|
const countQuery = `SELECT COUNT(*) as total FROM "${tableName}"`;
|
||||||
|
const countResult = await pool.query(countQuery);
|
||||||
|
reportContent += ` 📈 Nombre d'enregistrements: ${countResult.rows[0].total}\n\n`;
|
||||||
|
} catch (countError) {
|
||||||
|
reportContent += ` ⚠️ Impossible de compter les enregistrements: ${(countError as Error).message}\n\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer les contraintes de clés étrangères
|
||||||
|
reportContent += `CONTRAINTES DE CLÉS ÉTRANGÈRES:\n`;
|
||||||
|
reportContent += `===================================\n\n`;
|
||||||
|
|
||||||
|
const fkQuery = `
|
||||||
|
SELECT
|
||||||
|
tc.table_name,
|
||||||
|
kcu.column_name,
|
||||||
|
ccu.table_name AS foreign_table_name,
|
||||||
|
ccu.column_name AS foreign_column_name,
|
||||||
|
tc.constraint_name
|
||||||
|
FROM information_schema.table_constraints AS tc
|
||||||
|
JOIN information_schema.key_column_usage AS kcu
|
||||||
|
ON tc.constraint_name = kcu.constraint_name
|
||||||
|
AND tc.table_schema = kcu.table_schema
|
||||||
|
JOIN information_schema.constraint_column_usage AS ccu
|
||||||
|
ON ccu.constraint_name = tc.constraint_name
|
||||||
|
AND ccu.table_schema = tc.table_schema
|
||||||
|
WHERE tc.constraint_type = 'FOREIGN KEY'
|
||||||
|
AND tc.table_schema = 'public'
|
||||||
|
ORDER BY tc.table_name, kcu.column_name
|
||||||
|
`;
|
||||||
|
|
||||||
|
const fkResult = await pool.query(fkQuery);
|
||||||
|
|
||||||
|
if (fkResult.rows.length > 0) {
|
||||||
|
fkResult.rows.forEach((fk: any) => {
|
||||||
|
reportContent += ` ${fk.table_name}.${fk.column_name} → ${fk.foreign_table_name}.${fk.foreign_column_name}\n`;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
reportContent += ' Aucune contrainte de clé étrangère trouvée\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
reportContent += `\n===============================================\n`;
|
||||||
|
reportContent += `FIN DU RAPPORT - ${timestamp}\n`;
|
||||||
|
reportContent += `===============================================`;
|
||||||
|
|
||||||
|
// Définir les headers pour le téléchargement
|
||||||
|
const filename = `logiflow-schema-${new Date().toISOString().split('T')[0]}.txt`;
|
||||||
|
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||||
|
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
||||||
|
res.send(reportContent);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ ERREUR lors de la génération du rapport de schéma:', error);
|
||||||
|
res.status(500).json({ error: (error as Error).message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user