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:
michaelschal committed 2025-08-12 08:42:58 +00:00
1 parent 0949386425
commit 5fe9763763
2 files changed
+178 -22

No files matched your search

+33 -17
View File
@@ -1,7 +1,7 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
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";
export default function DatabaseDebug() {
@@ -82,23 +82,36 @@ export default function DatabaseDebug() {
</p>
</div>
<Button
onClick={handleLogSchema}
disabled={isLoading}
className="w-full"
>
{isLoading ? (
<>
<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
</>
<div className="flex gap-2">
<Button
onClick={handleLogSchema}
disabled={isLoading}
className="flex-1"
>
{isLoading ? (
<>
<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
</>
)}
</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 && (
<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">
📋 Consultez maintenant les logs de votre serveur pour voir le détail complet du schéma.
</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>
)}
+145 -5
View File
@@ -1448,7 +1448,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
const columnsResult = await pool.query(columnsQuery, [tableName]);
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 maxLength = col.character_maximum_length ? `(${col.character_maximum_length})` : '';
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);
console.log(` 📈 Nombre d'enregistrements: ${countResult.rows[0].total}`);
} 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);
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}`);
});
} 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',
totalTables: tablesResult.rows.length,
totalForeignKeys: fkResult.rows.length,
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
downloadUrl: '/api/debug/download-schema'
});
} catch (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 });
}
});