feat: Implementazione completa sistema di estrazione e trasferimento dati con auto-selezione chiave primaria
- Refactoring logica connessione database per gestire database specificato/non specificato - Implementazione selezione database tramite modale quando necessario - Aggiunta discovery robusta di tabelle, colonne e preview dati - Implementazione validazione completa query custom (solo SELECT, blocco SQL injection) - Aggiunta preview paginata dati per query custom - Implementazione mapping automatico dei campi - Implementazione auto-selezione chiave primaria: * Per tabelle database: rilevamento automatico PK * Per query custom: ricerca pattern comuni (id, key, etc.) * Per file Excel/CSV: ricerca pattern italiani (codice, numero, etc.) - Aggiunta logging dettagliato per debugging e tracciamento flusso - Implementazione sistema associazioni con fallback robusto - Miglioramento gestione errori e validazioni input utente - Test funzionali completi e validazione end-to-end Il sistema ora supporta completamente il flusso di estrazione dati da database/file verso API REST con gestione automatica delle chiavi e mapping intelligente.
This commit is contained in:
@@ -662,11 +662,18 @@ public partial class DataCoupler : ComponentBase
|
|||||||
// Clear mappings when changing sheet
|
// Clear mappings when changing sheet
|
||||||
ClearAllMappings();
|
ClearAllMappings();
|
||||||
|
|
||||||
// For file sources, always require manual key selection
|
// For file sources, try auto-selection and then require manual key selection if not found
|
||||||
sourceKeyField = "";
|
sourceKeyField = "";
|
||||||
suggestedPrimaryKey = "";
|
suggestedPrimaryKey = "";
|
||||||
requiresManualKeySelection = true;
|
requiresManualKeySelection = true;
|
||||||
|
|
||||||
|
// AUTO-SELECT della chiave per i file
|
||||||
|
if (fileSheets.ContainsKey(sheetName))
|
||||||
|
{
|
||||||
|
var columns = fileSheets[sheetName].ToList();
|
||||||
|
TryAutoSelectKeyForFile(columns);
|
||||||
|
}
|
||||||
|
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -968,26 +975,33 @@ public partial class DataCoupler : ComponentBase
|
|||||||
if (!string.IsNullOrEmpty(primaryKey))
|
if (!string.IsNullOrEmpty(primaryKey))
|
||||||
{
|
{
|
||||||
suggestedPrimaryKey = primaryKey;
|
suggestedPrimaryKey = primaryKey;
|
||||||
// Suggest the primary key but don't auto-select it
|
|
||||||
Logger.LogInformation("Primary key detected for table {TableName}: {PrimaryKey}", tableName, primaryKey);
|
// AUTO-SELECT: Imposta automaticamente il campo chiave se rilevato
|
||||||
|
sourceKeyField = primaryKey;
|
||||||
|
requiresManualKeySelection = false;
|
||||||
|
|
||||||
|
Logger.LogInformation("Chiave primaria rilevata e auto-selezionata per la tabella {TableName}: {PrimaryKey}", tableName, primaryKey);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// No primary key found, require manual selection
|
// No primary key found, require manual selection
|
||||||
requiresManualKeySelection = true;
|
requiresManualKeySelection = true;
|
||||||
Logger.LogInformation("No primary key found for table {TableName}, manual selection required", tableName);
|
sourceKeyField = "";
|
||||||
|
Logger.LogInformation("Nessuna chiave primaria trovata per la tabella {TableName}, selezione manuale richiesta", tableName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, "Error detecting primary key for table {TableName}", tableName);
|
Logger.LogError(ex, "Errore nel rilevamento della chiave primaria per la tabella {TableName}", tableName);
|
||||||
requiresManualKeySelection = true;
|
requiresManualKeySelection = true;
|
||||||
|
sourceKeyField = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// For non-database sources, always require manual selection
|
// For non-database sources, always require manual selection
|
||||||
requiresManualKeySelection = true;
|
requiresManualKeySelection = true;
|
||||||
|
sourceKeyField = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
@@ -2008,7 +2022,38 @@ public partial class DataCoupler : ComponentBase
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
var trimmedQuery = query.Trim();
|
var trimmedQuery = query.Trim();
|
||||||
return trimmedQuery.StartsWith("SELECT", StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
// Deve iniziare con SELECT
|
||||||
|
if (!trimmedQuery.StartsWith("SELECT", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Lista di parole chiave vietate per sicurezza
|
||||||
|
var forbiddenKeywords = new[]
|
||||||
|
{
|
||||||
|
"INSERT", "UPDATE", "DELETE", "DROP", "CREATE", "ALTER", "TRUNCATE",
|
||||||
|
"EXEC", "EXECUTE", "sp_", "xp_", "BULK", "OPENROWSET", "OPENDATASOURCE"
|
||||||
|
};
|
||||||
|
|
||||||
|
var upperQuery = trimmedQuery.ToUpperInvariant();
|
||||||
|
|
||||||
|
// Verifica che non contenga parole chiave vietate
|
||||||
|
foreach (var keyword in forbiddenKeywords)
|
||||||
|
{
|
||||||
|
if (upperQuery.Contains(keyword))
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Query rifiutata: contiene parola chiave vietata '{Keyword}'", keyword);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica che non contenga commenti SQL potenzialmente pericolosi
|
||||||
|
if (upperQuery.Contains("--") || upperQuery.Contains("/*"))
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Query rifiutata: contiene commenti SQL non consentiti");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -2028,6 +2073,12 @@ public partial class DataCoupler : ComponentBase
|
|||||||
cleanQuery = cleanQuery.Substring(0, cleanQuery.Length - 1).Trim();
|
cleanQuery = cleanQuery.Substring(0, cleanQuery.Length - 1).Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rimuove caratteri di controllo pericolosi
|
||||||
|
cleanQuery = System.Text.RegularExpressions.Regex.Replace(cleanQuery, @"[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]", "");
|
||||||
|
|
||||||
|
// Normalizza spazi multipli
|
||||||
|
cleanQuery = System.Text.RegularExpressions.Regex.Replace(cleanQuery, @"\s+", " ");
|
||||||
|
|
||||||
return cleanQuery;
|
return cleanQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2112,6 +2163,9 @@ public partial class DataCoupler : ComponentBase
|
|||||||
// Clear mappings quando cambia la query
|
// Clear mappings quando cambia la query
|
||||||
ClearAllMappings();
|
ClearAllMappings();
|
||||||
|
|
||||||
|
// AUTO-SELECT della chiave per query custom
|
||||||
|
TryAutoSelectKeyForQuery(queryColumns);
|
||||||
|
|
||||||
Logger.LogInformation("Query validata con successo: {ColumnCount} colonne", queryColumns.Count);
|
Logger.LogInformation("Query validata con successo: {ColumnCount} colonne", queryColumns.Count);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -2706,6 +2760,157 @@ public partial class DataCoupler : ComponentBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tenta di auto-selezionare una chiave per le query custom
|
||||||
|
/// </summary>
|
||||||
|
private void TryAutoSelectKeyForQuery(List<string> columns)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Reset stato chiave
|
||||||
|
sourceKeyField = "";
|
||||||
|
suggestedPrimaryKey = "";
|
||||||
|
requiresManualKeySelection = true;
|
||||||
|
|
||||||
|
// Pattern comuni per identificare possibili chiavi primarie
|
||||||
|
var keyPatterns = new[]
|
||||||
|
{
|
||||||
|
"id", "ID", "Id",
|
||||||
|
"_id", "_ID", "_Id",
|
||||||
|
"key", "KEY", "Key",
|
||||||
|
"code", "CODE", "Code",
|
||||||
|
"number", "NUMBER", "Number"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cerca colonne che potrebbero essere chiavi primarie
|
||||||
|
string? detectedKey = null;
|
||||||
|
|
||||||
|
// 1. Cerca esattamente "id", "ID", "Id"
|
||||||
|
detectedKey = columns.FirstOrDefault(c =>
|
||||||
|
c.Equals("id", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
c.Equals("ID", StringComparison.Ordinal) ||
|
||||||
|
c.Equals("Id", StringComparison.Ordinal));
|
||||||
|
|
||||||
|
// 2. Se non trovato, cerca colonne che terminano con "id", "ID", "Id"
|
||||||
|
if (detectedKey == null)
|
||||||
|
{
|
||||||
|
detectedKey = columns.FirstOrDefault(c =>
|
||||||
|
c.EndsWith("id", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
c.EndsWith("ID", StringComparison.Ordinal) ||
|
||||||
|
c.EndsWith("Id", StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Se non trovato, cerca colonne che contengono pattern di chiave
|
||||||
|
if (detectedKey == null)
|
||||||
|
{
|
||||||
|
foreach (var pattern in keyPatterns)
|
||||||
|
{
|
||||||
|
detectedKey = columns.FirstOrDefault(c =>
|
||||||
|
c.Contains(pattern, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (detectedKey != null) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Auto-seleziona se trovato
|
||||||
|
if (!string.IsNullOrEmpty(detectedKey))
|
||||||
|
{
|
||||||
|
sourceKeyField = detectedKey;
|
||||||
|
suggestedPrimaryKey = detectedKey;
|
||||||
|
requiresManualKeySelection = false;
|
||||||
|
|
||||||
|
Logger.LogInformation("Chiave auto-selezionata per query custom: {KeyField}", detectedKey);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogInformation("Nessuna chiave rilevabile automaticamente per query custom, selezione manuale richiesta");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Errore nell'auto-selezione della chiave per query custom");
|
||||||
|
sourceKeyField = "";
|
||||||
|
suggestedPrimaryKey = "";
|
||||||
|
requiresManualKeySelection = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tenta di auto-selezionare una chiave per i file (Excel/CSV)
|
||||||
|
/// </summary>
|
||||||
|
private void TryAutoSelectKeyForFile(List<string> columns)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Reset stato chiave
|
||||||
|
sourceKeyField = "";
|
||||||
|
suggestedPrimaryKey = "";
|
||||||
|
requiresManualKeySelection = true;
|
||||||
|
|
||||||
|
// Pattern comuni per identificare possibili chiavi primarie nei file
|
||||||
|
var keyPatterns = new[]
|
||||||
|
{
|
||||||
|
"id", "ID", "Id",
|
||||||
|
"_id", "_ID", "_Id",
|
||||||
|
"key", "KEY", "Key",
|
||||||
|
"code", "CODE", "Code", "codice", "CODICE", "Codice",
|
||||||
|
"number", "NUMBER", "Number", "numero", "NUMERO", "Numero",
|
||||||
|
"index", "INDEX", "Index", "indice", "INDICE", "Indice"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cerca colonne che potrebbero essere chiavi primarie
|
||||||
|
string? detectedKey = null;
|
||||||
|
|
||||||
|
// 1. Cerca esattamente "id", "ID", "Id"
|
||||||
|
detectedKey = columns.FirstOrDefault(c =>
|
||||||
|
c.Equals("id", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
c.Equals("ID", StringComparison.Ordinal) ||
|
||||||
|
c.Equals("Id", StringComparison.Ordinal) ||
|
||||||
|
c.Equals("codice", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
// 2. Se non trovato, cerca colonne che terminano con pattern comuni
|
||||||
|
if (detectedKey == null)
|
||||||
|
{
|
||||||
|
foreach (var pattern in keyPatterns.Take(6)) // Solo i primi pattern più comuni
|
||||||
|
{
|
||||||
|
detectedKey = columns.FirstOrDefault(c =>
|
||||||
|
c.EndsWith(pattern, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (detectedKey != null) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Se non trovato, cerca colonne che contengono pattern di chiave
|
||||||
|
if (detectedKey == null)
|
||||||
|
{
|
||||||
|
foreach (var pattern in keyPatterns)
|
||||||
|
{
|
||||||
|
detectedKey = columns.FirstOrDefault(c =>
|
||||||
|
c.Contains(pattern, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (detectedKey != null) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Auto-seleziona se trovato
|
||||||
|
if (!string.IsNullOrEmpty(detectedKey))
|
||||||
|
{
|
||||||
|
sourceKeyField = detectedKey;
|
||||||
|
suggestedPrimaryKey = detectedKey;
|
||||||
|
requiresManualKeySelection = false;
|
||||||
|
|
||||||
|
Logger.LogInformation("Chiave auto-selezionata per file: {KeyField}", detectedKey);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogInformation("Nessuna chiave rilevabile automaticamente per file, selezione manuale richiesta");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Errore nell'auto-selezione della chiave per file");
|
||||||
|
sourceKeyField = "";
|
||||||
|
suggestedPrimaryKey = "";
|
||||||
|
requiresManualKeySelection = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user