Files
Data-Coupler/Data_Coupler/Pages/DataCoupler.razor.cs
T
Alessio 1435c013d3 Summarized conversation historyEcco il messaggio di commit tradotto in italiano:
**Refactoring: Separazione logica C# da markup Razor nel componente DataCoupler**

**Modifiche principali:**
- Spostamento di tutta la logica C# dal file DataCoupler.razor al file DataCoupler.razor.cs come partial class
- Rimozione completa del blocco @code dal file .razor mantenendo solo il markup HTML/Razor
- Aggiunta delle using directive mancanti (System.Data, System.Text, ExcelDataReader, ecc.)
- Correzione delle firme dei metodi e degli handler di eventi per la compatibilità Blazor
- Sistemazione delle proprietà di iniezione dei servizi [Inject] nel code-behind
- Risoluzione di tutti gli errori di compilazione relativi alla separazione dei file

**Miglioramenti strutturali:**
- Migliore separazione delle responsabilità tra presentazione e logica business
- Struttura del codice più pulita e manutenibile seguendo le best practice Blazor
- Codice più facilmente testabile con la logica isolata nel file .cs
- Rimozione di codice duplicato e ottimizzazione delle funzioni di utilità

**File modificati:**
- Data_Coupler/Pages/DataCoupler.razor: Pulizia markup, rimozione blocco @code
- Data_Coupler/Pages/DataCoupler.razor.cs: Implementazione completa della logica C# come partial class

Questo refactoring migliora significativamente la struttura del codice seguendo le convenzioni standard di Blazor per la separazione tra markup e logica applicativa.
2025-07-01 20:50:25 +02:00

2019 lines
79 KiB
C#

using System;
using System.Data;
using System.Text;
using CredentialManager.Models;
using DataConnection.Interfaces;
using DataConnection.REST.Interfaces;
using DataConnection.REST.Models;
using ExcelDataReader;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.JSInterop;
namespace Data_Coupler.Pages;
public partial class DataCoupler
{
// Classe per i risultati del trasferimento
public class TransferResult
{
public int RecordNumber { get; set; }
public string Status { get; set; } = ""; // "success", "error", "updated", "duplicate"
public string Message { get; set; } = "";
public string? EntityId { get; set; }
public Dictionary<string, object> RecordData { get; set; } = new();
}
// Stato delle credenziali
private List<DatabaseCredential> databaseCredentials = new();
private List<RestApiCredential> restApiCredentials = new();
// Selezione tipo fonte
private string selectedSourceType = "";
// Credenziali selezionate
private string selectedDatabaseCredential = "";
private string selectedRestCredential = "";
// Stato connessioni
private bool isConnectingDatabase = false;
private bool isConnectingRest = false;
private bool isDatabaseConnected = false;
private bool isRestConnected = false;
// Messaggi di errore
private string databaseErrorMessage = "";
private string restErrorMessage = "";
// Database discovery
private Dictionary<string, IEnumerable<DbColumnInfo>> databaseTables = new();
private string selectedTable = "";
private string databaseSearchTerm = "";
// Database selection
private List<string> availableDatabases = new();
private string selectedDatabase = "";
private bool showDatabaseSelection = false;
private bool showDatabaseSelectionModal = false;
private bool isLoadingDatabases = false;
// Custom query functionality
private bool useCustomQuery = false;
private string customQuery = "";
private bool isValidatingQuery = false;
private bool isQueryValid = false;
private string queryValidationMessage = "";
private List<Dictionary<string, object>> queryPreviewData = new();
private List<string> queryColumns = new();
private bool showQueryPreview = false;
private bool isLoadingPreview = false; // File handling
private string selectedFileName = "";
private bool isProcessingFile = false;
private string fileErrorMessage = "";
private Dictionary<string, IEnumerable<string>> fileSheets = new(); // SheetName -> Columns
private Dictionary<string, List<Dictionary<string, object>>> fileData = new(); // SheetName -> Data rows
private string selectedSheet = "";
// File preview pagination
private int currentPage = 1;
private int pageSize = 20;
private int GetTotalPages(string sheetName) => fileData.ContainsKey(sheetName) ?
(int)Math.Ceiling((double)fileData[sheetName].Count / pageSize) : 0;
// REST discovery
private List<RestEntitySummary> restEntities = new();
private RestEntitySummary? selectedRestEntity = null;
private RestEntityInfo? restEntityDetails = null;
private string restSearchTerm = "";
// Mapping campi
private Dictionary<string, string> fieldMappings = new(); // DbColumn -> RestProperty
private HashSet<string> keyFields = new(); // REST properties marked as keys
private string selectedDbColumn = "";
private string selectedRestProperty = "";
// Gestione chiavi sorgente e associazioni
private string sourceKeyField = ""; // Campo che identifica univocamente il record sorgente
private string suggestedPrimaryKey = ""; // Campo PK suggerito per database
private bool requiresManualKeySelection = false; // Flag per indicare se è richiesta selezione manuale
private Dictionary<string, string> sourceKeyMappings = new(); // Per CSV: mapppatura colonna -> nome campo chiave
private bool useRecordAssociations = true; // Se utilizzare il sistema di associazioni
// Trasferimento dati
private bool isTransferringData = false;
private string transferMessage = "";
private string transferMessageType = "";
private List<TransferResult> transferResults = new();
private bool showDetailedResults = false;
// Servizi
private IDatabaseManager? currentDatabaseManager = null;
private IRestMetadataDiscovery? currentRestDiscovery = null;
private IRestServiceClient? currentRestClient = null;
protected override async Task OnInitializedAsync()
{
await LoadCredentials();
} private async Task LoadCredentials()
{
try
{
databaseCredentials = await CredentialService.GetAllDatabaseCredentialsAsync();
restApiCredentials = await CredentialService.GetAllRestApiCredentialsAsync();
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nel caricamento delle credenziali");
await JSRuntime.InvokeVoidAsync("alert", $"Errore nel caricamento delle credenziali: {ex.Message}");
}
}
private void OnSourceTypeChanged(ChangeEventArgs e)
{
selectedSourceType = e.Value?.ToString() ?? "";
// Reset state when changing source type
ResetSourceState(); } private void ResetSourceState()
{
// Reset database state
ResetDatabaseState();
// Reset file state
selectedFileName = "";
isProcessingFile = false;
fileErrorMessage = "";
fileSheets.Clear();
fileData.Clear();
selectedSheet = "";
// Reset pagination
currentPage = 1;
// Reset mappings
ClearAllMappings();
}
private async Task OnFileSelected(InputFileChangeEventArgs e)
{ try
{
isProcessingFile = true;
fileErrorMessage = "";
fileSheets.Clear();
fileData.Clear();
selectedSheet = "";
var file = e.File;
selectedFileName = file.Name;
// Validate file type
var extension = Path.GetExtension(file.Name).ToLowerInvariant();
if (extension != ".xlsx" && extension != ".xls" && extension != ".csv")
{
fileErrorMessage = "Formato file non supportato. Utilizzare Excel (.xlsx, .xls) o CSV (.csv)";
return;
}
// Process file based on type
if (extension == ".csv")
{
await ProcessCsvFile(file);
}
else
{
await ProcessExcelFile(file);
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nell'elaborazione del file");
fileErrorMessage = $"Errore nell'elaborazione del file: {ex.Message}";
}
finally
{
isProcessingFile = false;
StateHasChanged();
}
} private async Task ProcessCsvFile(IBrowserFile file)
{
using var stream = file.OpenReadStream(maxAllowedSize: 50 * 1024 * 1024); // Aumentato a 50MB
using var reader = new StreamReader(stream);
var firstLine = await reader.ReadLineAsync();
if (string.IsNullOrEmpty(firstLine))
{
fileErrorMessage = "Il file CSV è vuoto";
return;
}
Logger.LogInformation("CSV first line: {FirstLine}", firstLine);
// Detect separator automatically
var separator = DetectCsvSeparator(firstLine);
Logger.LogInformation("CSV separator detected: '{Separator}'", separator);
// Parse headers (first row) - gestisce meglio i separatori
var headers = ParseCsvLine(firstLine, separator);
Logger.LogInformation("CSV headers parsed: {Headers}", string.Join(" | ", headers));
// For CSV, we create a single "sheet" with the filename
var sheetName = Path.GetFileNameWithoutExtension(file.Name);
fileSheets[sheetName] = headers;
// Read data rows - rimuovo il limite di 1000 righe
var dataRows = new List<Dictionary<string, object>>();
string? line;
int rowNumber = 2; // Starting from row 2 (after header)
while ((line = await reader.ReadLineAsync()) != null)
{
if (string.IsNullOrWhiteSpace(line)) continue;
var values = ParseCsvLine(line, separator);
var row = new Dictionary<string, object>();
for (int i = 0; i < headers.Count; i++)
{
var value = i < values.Count ? values[i] : "";
row[headers[i]] = string.IsNullOrEmpty(value) ? "" : value;
}
dataRows.Add(row);
rowNumber++;
// Log delle prime 3 righe per debug
if (rowNumber <= 5)
{
Logger.LogInformation("CSV row {RowNumber}: {Values}", rowNumber - 1, string.Join(" | ", values));
}
}
fileData[sheetName] = dataRows;
// Auto-seleziona il foglio per i CSV dato che ce n'è solo uno
selectedSheet = sheetName;
Logger.LogInformation("CSV file processed: {FileName}, Headers: {HeaderCount} ({Headers}), Rows: {RowCount}, Auto-selected sheet: {SheetName}",
file.Name, headers.Count, string.Join(", ", headers), dataRows.Count, selectedSheet);
} private List<string> ParseCsvLine(string line, char separator = ',')
{
var result = new List<string>();
var current = new StringBuilder();
bool inQuotes = false;
for (int i = 0; i < line.Length; i++)
{
char c = line[i];
if (c == '"')
{
if (inQuotes && i + 1 < line.Length && line[i + 1] == '"')
{
// Double quote - escaped quote
current.Append('"');
i++; // Skip next quote
}
else
{
// Toggle quote mode
inQuotes = !inQuotes;
}
}
else if (c == separator && !inQuotes)
{
// End of field
result.Add(current.ToString().Trim());
current.Clear();
}
else
{
current.Append(c);
}
}
// Add the last field
result.Add(current.ToString().Trim());
return result;
}private async Task ProcessExcelFile(IBrowserFile file)
{
try
{
using var stream = file.OpenReadStream(maxAllowedSize: 50 * 1024 * 1024); // 50MB max
// Crea il reader Excel basato sull'estensione
IExcelDataReader reader;
var extension = Path.GetExtension(file.Name).ToLowerInvariant();
if (extension == ".xlsx")
{
reader = ExcelReaderFactory.CreateOpenXmlReader(stream);
}
else if (extension == ".xls")
{
reader = ExcelReaderFactory.CreateBinaryReader(stream);
}
else
{
fileErrorMessage = "Formato Excel non supportato. Utilizzare .xlsx o .xls";
return;
}
using (reader)
{
// Configura per utilizzare la prima riga come header
var configuration = new ExcelDataSetConfiguration()
{
ConfigureDataTable = (_) => new ExcelDataTableConfiguration()
{
UseHeaderRow = true // Prima riga come header
}
};
// Converti in DataSet
var dataSet = reader.AsDataSet(configuration);
Logger.LogInformation("Excel file processed: {FileName}, Sheets: {SheetCount}",
file.Name, dataSet.Tables.Count);
// Processa ogni foglio
foreach (DataTable table in dataSet.Tables)
{
var sheetName = table.TableName;
var headers = new List<string>();
var dataRows = new List<Dictionary<string, object>>();
// Estrai i nomi delle colonne (headers)
foreach (DataColumn column in table.Columns)
{
headers.Add(column.ColumnName);
}
Logger.LogInformation("Processing Excel sheet: {SheetName}, Columns: {ColumnCount}, Rows: {RowCount}",
sheetName, headers.Count, table.Rows.Count);
// Processa le righe di dati
for (int i = 0; i < table.Rows.Count; i++)
{
var row = table.Rows[i];
var rowData = new Dictionary<string, object>();
for (int j = 0; j < headers.Count; j++)
{
var cellValue = row[j]?.ToString() ?? "";
rowData[headers[j]] = string.IsNullOrEmpty(cellValue) ? "" : cellValue;
}
dataRows.Add(rowData);
// Log delle prime 3 righe per debug
if (i < 3)
{
Logger.LogInformation("Excel row {RowNumber} in {Sheet}: {Values}",
i + 1, sheetName, string.Join(" | ", rowData.Values));
}
}
// Salva i dati del foglio
fileSheets[sheetName] = headers;
fileData[sheetName] = dataRows;
Logger.LogInformation("Excel sheet completed: {SheetName}, Headers: {Headers}, Rows: {RowCount}",
sheetName, string.Join(", ", headers), dataRows.Count);
}
// Auto-seleziona il primo foglio se non c'è una selezione
if (fileSheets.Any() && string.IsNullOrEmpty(selectedSheet))
{
selectedSheet = fileSheets.First().Key;
Logger.LogInformation("Auto-selected first sheet: {SheetName}", selectedSheet);
} Logger.LogInformation("Excel file processing completed: {FileName}, Total sheets: {SheetCount}, Selected: {SelectedSheet}",
file.Name, fileSheets.Count, selectedSheet);
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nell'elaborazione del file Excel: {FileName}", file.Name);
fileErrorMessage = $"Errore nell'elaborazione del file Excel: {ex.Message}";
}
await Task.CompletedTask;
} private void SelectSheet(string sheetName)
{
selectedSheet = sheetName;
// Reset pagination when changing sheet
currentPage = 1;
// Clear mappings when changing sheet
ClearAllMappings();
// For file sources, always require manual key selection
sourceKeyField = "";
suggestedPrimaryKey = "";
requiresManualKeySelection = true;
StateHasChanged();
}
// File preview pagination methods
private void GoToPage(int page)
{
if (string.IsNullOrEmpty(selectedSheet) || !fileData.ContainsKey(selectedSheet))
return;
var totalPages = GetTotalPages(selectedSheet);
if (page >= 1 && page <= totalPages)
{
currentPage = page;
StateHasChanged();
}
}
private void FirstPage() => GoToPage(1);
private void PreviousPage() => GoToPage(currentPage - 1);
private void NextPage() => GoToPage(currentPage + 1);
private void LastPage() => GoToPage(GetTotalPages(selectedSheet));
private List<Dictionary<string, object>> GetCurrentPageData()
{
if (string.IsNullOrEmpty(selectedSheet) || !fileData.ContainsKey(selectedSheet))
return new List<Dictionary<string, object>>();
var allData = fileData[selectedSheet];
var skip = (currentPage - 1) * pageSize;
return allData.Skip(skip).Take(pageSize).ToList();
}
private int GetStartRecord()
{
if (string.IsNullOrEmpty(selectedSheet) || !fileData.ContainsKey(selectedSheet))
return 0;
return (currentPage - 1) * pageSize + 1;
} private int GetEndRecord()
{
if (string.IsNullOrEmpty(selectedSheet) || !fileData.ContainsKey(selectedSheet))
return 0;
var totalRecords = fileData[selectedSheet].Count;
var endRecord = currentPage * pageSize;
return Math.Min(endRecord, totalRecords);
}
private void OnPageSizeChanged(ChangeEventArgs e)
{
if (int.TryParse(e.Value?.ToString(), out int newPageSize))
{
pageSize = newPageSize;
currentPage = 1; // Reset to first page when changing page size
StateHasChanged();
}
}private void OnDatabaseCredentialChanged(ChangeEventArgs e)
{
selectedDatabaseCredential = e.Value?.ToString() ?? "";
ResetDatabaseState();
} private void OnRestCredentialChanged(ChangeEventArgs e)
{
var newCredential = e.Value?.ToString() ?? "";
// Clear the cache if we're switching to a different credential
if (!string.IsNullOrEmpty(selectedRestCredential) && selectedRestCredential != newCredential)
{
ConnectionFactory.ClearRestClientCache(selectedRestCredential);
Logger.LogInformation("Cleared REST client cache for credential: {CredentialName}", selectedRestCredential);
}
selectedRestCredential = newCredential;
ResetRestState();
} private void ResetDatabaseState()
{
isDatabaseConnected = false;
databaseTables.Clear();
selectedTable = "";
databaseSearchTerm = "";
databaseErrorMessage = "";
// Reset custom query state
useCustomQuery = false;
customQuery = "";
isValidatingQuery = false;
isQueryValid = false;
queryValidationMessage = "";
queryPreviewData.Clear();
queryColumns.Clear();
showQueryPreview = false;
isLoadingPreview = false;
currentDatabaseManager?.Dispose();
currentDatabaseManager = null;
// Clear mappings when resetting database state
ClearAllMappings();
} private void ResetRestState()
{
isRestConnected = false;
restEntities.Clear();
selectedRestEntity = null;
restEntityDetails = null;
restSearchTerm = "";
restErrorMessage = "";
currentRestDiscovery = null;
currentRestClient = null;
// Clear mappings when resetting REST state
ClearAllMappings();
}private async Task ConnectToDatabase()
{
if (string.IsNullOrEmpty(selectedDatabaseCredential))
return;
isConnectingDatabase = true;
databaseErrorMessage = "";
try
{ // Trova la credenziale
var credential = databaseCredentials.FirstOrDefault(c => c.Name == selectedDatabaseCredential);
if (credential == null)
{
databaseErrorMessage = "Credenziale database non trovata";
return;
}
// Test della connessione
var (success, message) = await CredentialService.TestDatabaseConnectionAsync(credential.Name);
if (!success)
{
databaseErrorMessage = $"Connessione fallita: {message}";
return;
} // Crea il database manager usando il factory con le credenziali complete
Logger.LogInformation("Creando database manager per credenziale: {CredentialName}", selectedDatabaseCredential);
currentDatabaseManager = await ConnectionFactory.CreateDatabaseManagerAsync(selectedDatabaseCredential);
Logger.LogInformation("Database manager creato con successo");
Logger.LogInformation("Iniziando discovery dello schema per database {DatabaseType} con credenziale: {CredentialName}", credential.DatabaseType, selectedDatabaseCredential);
// Discovery dello schema con try-catch specifico
try
{
var schema = await currentDatabaseManager.GetDatabaseSchemaAsync();
Logger.LogInformation("Schema discovery completato. Tipo restituito: {SchemaType}, Numero elementi: {Count}",
schema?.GetType().Name ?? "null",
schema?.Count() ?? 0);
databaseTables = schema as Dictionary<string, IEnumerable<DbColumnInfo>> ??
(schema != null ? new Dictionary<string, IEnumerable<DbColumnInfo>>(schema) : new Dictionary<string, IEnumerable<DbColumnInfo>>());
Logger.LogInformation("Database tables dopo conversione: {Count} tabelle", databaseTables.Count);
if (databaseTables.Count == 0)
{
// Se non ci sono tabelle, potrebbe essere perché non è stato selezionato un database specifico
await HandleDatabaseSelectionRequired();
return;
}
}
catch (Exception schemaEx)
{
Logger.LogError(schemaEx, "Errore specifico durante lo schema discovery");
databaseErrorMessage = $"Errore nello schema discovery: {schemaEx.Message}";
throw;
}
isDatabaseConnected = true;
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nella connessione al database");
databaseErrorMessage = $"Errore: {ex.Message}";
}
finally
{
isConnectingDatabase = false;
}
} private async Task ConnectToRestApi()
{
if (string.IsNullOrEmpty(selectedRestCredential))
return;
isConnectingRest = true;
restErrorMessage = "";
try
{
// Trova la credenziale
var credential = restApiCredentials.FirstOrDefault(c => c.Name == selectedRestCredential);
if (credential == null)
{
restErrorMessage = "Credenziale REST API non trovata";
return;
}
// Test della connessione
var (success, message) = await CredentialService.TestRestApiConnectionAsync(credential.Name);
if (!success)
{
restErrorMessage = $"Connessione fallita: {message}";
return;
} // Crea i client REST usando il factory con le credenziali complete
currentRestClient = await ConnectionFactory.CreateRestServiceClientAsync(selectedRestCredential);
currentRestDiscovery = await ConnectionFactory.CreateRestMetadataDiscoveryAsync(selectedRestCredential); Logger.LogInformation("Iniziando autenticazione per il servizio REST {ServiceType} con credenziale: {CredentialName}", credential.ServiceType, selectedRestCredential);
// Autenticazione prima del discovery
var authResult = await currentRestClient.AuthenticateAsync();
if (!authResult)
{
Logger.LogWarning("Autenticazione fallita per il servizio REST {ServiceType}", credential.ServiceType);
restErrorMessage = "Autenticazione fallita per il servizio REST";
return;
}
Logger.LogInformation("Autenticazione completata. Iniziando discovery delle entità REST per {ServiceType}", credential.ServiceType);
// Discovery delle entità disponibili
restEntities = await currentRestDiscovery.DiscoverEntitySummariesAsync();
Logger.LogInformation("Discovery completato. Trovate {Count} entità", restEntities?.Count ?? 0);
if (restEntities == null || !restEntities.Any())
{
Logger.LogWarning("Nessuna entità trovata dal servizio REST");
restErrorMessage = "Nessuna entità disponibile dal servizio REST";
return;
}
isRestConnected = true;
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nella connessione al servizio REST");
restErrorMessage = $"Errore: {ex.Message}";
}
finally
{
isConnectingRest = false;
}
} private async void SelectTable(string tableName)
{
selectedTable = tableName;
// Clear custom query state when selecting a table
useCustomQuery = false;
customQuery = "";
isQueryValid = false;
queryValidationMessage = "";
queryPreviewData.Clear();
queryColumns.Clear();
showQueryPreview = false;
// Clear mappings when changing table
ClearAllMappings();
// Reset key field logic
sourceKeyField = "";
suggestedPrimaryKey = "";
requiresManualKeySelection = false;
// If it's a database source, try to detect the primary key
if (selectedSourceType == "database" && currentDatabaseManager != null)
{
try
{
var primaryKey = await currentDatabaseManager.GetPrimaryKeyFieldAsync(tableName);
if (!string.IsNullOrEmpty(primaryKey))
{
suggestedPrimaryKey = primaryKey;
// Suggest the primary key but don't auto-select it
Logger.LogInformation("Primary key detected for table {TableName}: {PrimaryKey}", tableName, primaryKey);
}
else
{
// No primary key found, require manual selection
requiresManualKeySelection = true;
Logger.LogInformation("No primary key found for table {TableName}, manual selection required", tableName);
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Error detecting primary key for table {TableName}", tableName);
requiresManualKeySelection = true;
}
}
else
{
// For non-database sources, always require manual selection
requiresManualKeySelection = true;
}
StateHasChanged();
} private async Task SelectRestEntity(RestEntitySummary entity)
{
selectedRestEntity = entity;
// Clear mappings when changing entity
ClearAllMappings();
try
{
if (currentRestDiscovery != null)
{
// Discovery dei dettagli dell'entità
restEntityDetails = await currentRestDiscovery.DiscoverEntityDetailsAsync(entity.Name); }
else
{
restErrorMessage = "Servizio di discovery REST non disponibile";
return;
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nel caricamento dettagli entità {EntityName}", entity.Name);
restErrorMessage = $"Errore nel caricamento dettagli entità: {ex.Message}";
} }
// Metodi per la ricerca e il filtraggio
private IEnumerable<string> GetFilteredDatabaseTables()
{
if (string.IsNullOrEmpty(databaseSearchTerm))
return databaseTables.Keys;
return databaseTables.Keys.Where(table =>
table.Contains(databaseSearchTerm, StringComparison.OrdinalIgnoreCase));
}
private IEnumerable<RestEntitySummary> GetFilteredRestEntities()
{
if (string.IsNullOrEmpty(restSearchTerm))
return restEntities;
return restEntities.Where(entity =>
entity.Name.Contains(restSearchTerm, StringComparison.OrdinalIgnoreCase) ||
(!string.IsNullOrEmpty(entity.Label) && entity.Label.Contains(restSearchTerm, StringComparison.OrdinalIgnoreCase)));
}
private async Task FilterDatabaseTables(ChangeEventArgs e)
{
databaseSearchTerm = e.Value?.ToString() ?? "";
await InvokeAsync(StateHasChanged);
}
private async Task FilterRestEntities(ChangeEventArgs e)
{
restSearchTerm = e.Value?.ToString() ?? "";
await InvokeAsync(StateHasChanged);
}
private async Task ClearDatabaseSearch()
{
databaseSearchTerm = "";
await InvokeAsync(StateHasChanged);
}
private async Task ClearRestSearch()
{
restSearchTerm = "";
await InvokeAsync(StateHasChanged);
}
// Metodi per il mapping dei campi
private void SelectDbColumn(string columnName)
{
selectedDbColumn = columnName;
}
private void SelectRestProperty(string propertyName)
{
selectedRestProperty = propertyName;
}
private void CreateMapping()
{
if (string.IsNullOrEmpty(selectedDbColumn) || string.IsNullOrEmpty(selectedRestProperty))
return;
// Rimuovi eventuali mapping esistenti per questo campo database
if (fieldMappings.ContainsKey(selectedDbColumn))
{
fieldMappings.Remove(selectedDbColumn);
}
// Crea il nuovo mapping
fieldMappings[selectedDbColumn] = selectedRestProperty;
Logger.LogInformation("Creato mapping: {DbColumn} -> {RestProperty}", selectedDbColumn, selectedRestProperty);
// Deseleziona i campi
selectedDbColumn = "";
selectedRestProperty = "";
}
private void RemoveMapping()
{
if (string.IsNullOrEmpty(selectedDbColumn) || !fieldMappings.ContainsKey(selectedDbColumn))
return;
fieldMappings.Remove(selectedDbColumn);
Logger.LogInformation("Rimosso mapping per campo: {DbColumn}", selectedDbColumn);
} private void RemoveSpecificMapping(string dbColumn)
{
if (fieldMappings.ContainsKey(dbColumn))
{
fieldMappings.Remove(dbColumn);
Logger.LogInformation("Rimosso mapping specifico per campo: {DbColumn}", dbColumn);
}
}
private void ClearAllMappings()
{
fieldMappings.Clear();
selectedDbColumn = "";
selectedRestProperty = "";
sourceKeyField = "";
transferMessage = "";
transferMessageType = "";
Logger.LogInformation("Tutti i mapping e le configurazioni sono stati cancellati");
}
private void AutoMapFields()
{
if (restEntityDetails == null)
return;
IEnumerable<string> sourceColumns = new List<string>();
// Ottiene le colonne in base al tipo di sorgente
if (selectedSourceType == "database")
{
if (useCustomQuery && queryColumns.Any())
{
sourceColumns = queryColumns;
}
else if (!useCustomQuery && databaseTables.ContainsKey(selectedTable))
{
sourceColumns = databaseTables[selectedTable].Select(c => c.Name);
}
}
else if (selectedSourceType == "file" && fileSheets.ContainsKey(selectedSheet))
{
sourceColumns = fileSheets[selectedSheet];
}
if (!sourceColumns.Any())
return;
var restProperties = restEntityDetails.Properties;
int mappingsCreated = 0;
foreach (var sourceColumn in sourceColumns)
{
// Trova una proprietà REST con nome simile
var matchingProperty = restProperties.FirstOrDefault(p =>
string.Equals(p.Name, sourceColumn, StringComparison.OrdinalIgnoreCase) ||
string.Equals(p.Name.Replace("_", ""), sourceColumn.Replace("_", ""), StringComparison.OrdinalIgnoreCase) ||
string.Equals(p.Name.Replace("Id", ""), sourceColumn.Replace("Id", ""), StringComparison.OrdinalIgnoreCase)
);
if (matchingProperty != null && !fieldMappings.ContainsKey(sourceColumn))
{
fieldMappings[sourceColumn] = matchingProperty.Name;
mappingsCreated++;
}
}
Logger.LogInformation("Auto-mapping completato. Creati {Count} mapping automatici da {SourceType}",
mappingsCreated, useCustomQuery ? "query custom" : selectedSourceType);
} private async Task ShowMappingSummary()
{
var summary = "Riepilogo Configurazione:\n\n";
summary += "=== MAPPING CAMPI ===\n";
foreach (var mapping in fieldMappings)
{
summary += $"• {mapping.Key} → {mapping.Value}\n";
}
summary += "\n=== CONFIGURAZIONE ASSOCIAZIONI ===\n";
summary += $"• Sistema associazioni: {(useRecordAssociations ? "Abilitato" : "Disabilitato")}\n";
if (useRecordAssociations)
{
summary += $"• Campo chiave sorgente: {(!string.IsNullOrEmpty(sourceKeyField) ? sourceKeyField : "Rilevamento automatico")}\n";
}
await JSRuntime.InvokeVoidAsync("alert", summary);
} private async Task StartDataTransfer()
{
if (!fieldMappings.Any() || currentRestClient == null || selectedRestEntity == null)
{
transferMessage = "Configurazione incompleta. Assicurati di aver selezionato la fonte dati, entità e configurato almeno una mappatura.";
transferMessageType = "error";
return;
}
// Check source-specific requirements
if (selectedSourceType == "database")
{
if (currentDatabaseManager == null)
{
transferMessage = "Database non connesso.";
transferMessageType = "error";
return;
}
if (useCustomQuery)
{
if (!isQueryValid || string.IsNullOrWhiteSpace(customQuery))
{
transferMessage = "Query custom non valida. Validare la query prima di procedere.";
transferMessageType = "error";
return;
}
}
else if (string.IsNullOrEmpty(selectedTable))
{
transferMessage = "Tabella non selezionata.";
transferMessageType = "error";
return;
}
}
if (selectedSourceType == "file" && string.IsNullOrEmpty(selectedSheet))
{
transferMessage = "File non caricato o foglio non selezionato.";
transferMessageType = "error";
return;
}
// Validate source key field when using record associations
if (useRecordAssociations && string.IsNullOrEmpty(sourceKeyField))
{
transferMessage = "Campo chiave sorgente richiesto. Seleziona un campo che identifichi univocamente ogni record per utilizzare il sistema di associazioni.";
transferMessageType = "error";
return;
}
isTransferringData = true;
transferMessage = "";
transferMessageType = "";
transferResults.Clear();
try
{
var sourceName = selectedSourceType == "database"
? (useCustomQuery ? "custom_query" : selectedTable)
: selectedSheet;
Logger.LogInformation("Iniziando trasferimento dati da {SourceType} {Source} a {Entity} con {MappingCount} mappature",
selectedSourceType, sourceName, selectedRestEntity.Name, fieldMappings.Count);
// 1. Ottieni tutti i record dalla fonte dati
var records = await GetAllRecordsFromSource();
Logger.LogInformation("Ottenuti {RecordCount} record da {SourceType} {Source}", records.Count(), selectedSourceType, sourceName);
if (!records.Any())
{
transferMessage = "Nessun record trovato nella fonte dati selezionata.";
transferMessageType = "error";
return;
}
// 2. Ottieni i campi obbligatori dell'entità REST (se non ci sono campi chiave)
var requiredFields = new HashSet<string>();
if (!keyFields.Any() && restEntityDetails != null)
{
requiredFields = restEntityDetails.Properties
.Where(p => p.IsRequired && fieldMappings.ContainsValue(p.Name))
.Select(p => p.Name)
.ToHashSet();
Logger.LogInformation("Nessun campo chiave definito. Utilizzo {RequiredFieldsCount} campi obbligatori per controllo duplicati: {RequiredFields}",
requiredFields.Count, string.Join(", ", requiredFields));
}
// 3. Trasforma e trasferisci ogni record
int successCount = 0;
int errorCount = 0;
int updatedCount = 0;
int duplicateCount = 0;
var errors = new List<string>();
int recordNumber = 1;
foreach (var record in records)
{
var transferResult = new TransferResult
{
RecordNumber = recordNumber,
RecordData = new Dictionary<string, object>(record)
};
try
{
// Trasforma il record in base ai mapping
var restData = TransformRecordToRestEntity(record);
// Genera la chiave sorgente per questo record
var sourceKey = GenerateSourceKey(record);
// NUOVO SISTEMA: Cerca associazione esistente basata sul valore della chiave
if (useRecordAssociations && !string.IsNullOrEmpty(sourceKey))
{
Logger.LogInformation("ASSOCIATION DEBUG: Cerco associazione - KeyValue: '{KeyValue}', Entity: '{Entity}', Credential: '{Credential}'",
sourceKey, selectedRestEntity.Name, selectedRestCredential);
// Cerca se esiste già un'associazione per questo valore chiave
var existingAssociation = await CredentialService.FindKeyAssociationByValueAsync(
sourceKey, selectedRestEntity.Name, selectedRestCredential);
// FALLBACK: Se non troviamo l'associazione con tutti i parametri, proviamo solo con il KeyValue
if (existingAssociation == null)
{
Logger.LogWarning("ASSOCIATION DEBUG: Associazione non trovata con parametri specifici, provo solo con KeyValue: '{KeyValue}'", sourceKey);
existingAssociation = await CredentialService.FindKeyAssociationByValueAsync(sourceKey);
if (existingAssociation != null)
{
Logger.LogWarning("ASSOCIATION DEBUG: Trovata associazione con fallback - ID: {AssociationId}, Entity: '{Entity}', Credential: '{Credential}'",
existingAssociation.Id, existingAssociation.DestinationEntity, existingAssociation.RestCredentialName);
// Verifica se l'associazione trovata è compatibile
if (existingAssociation.DestinationEntity != selectedRestEntity.Name ||
existingAssociation.RestCredentialName != selectedRestCredential)
{
Logger.LogWarning("ASSOCIATION DEBUG: Associazione non compatibile - Entity: '{FoundEntity}' vs '{ExpectedEntity}', Credential: '{FoundCredential}' vs '{ExpectedCredential}'",
existingAssociation.DestinationEntity, selectedRestEntity.Name, existingAssociation.RestCredentialName, selectedRestCredential);
existingAssociation = null;
}
}
}
Logger.LogInformation("ASSOCIATION DEBUG: Associazione finale: {Found}. ID: {AssociationId}, DestinationId: '{DestinationId}', IsActive: {IsActive}",
existingAssociation != null, existingAssociation?.Id, existingAssociation?.DestinationId, existingAssociation?.IsActive);
if (existingAssociation != null && existingAssociation.IsActive)
{
// Prova direttamente l'aggiornamento - più efficiente che verificare prima l'esistenza
Logger.LogInformation("ASSOCIATION DEBUG: Tentativo aggiornamento record esistente - DestinationId: '{DestinationId}'", existingAssociation.DestinationId);
try
{
var updateResult = await currentRestClient.UpdateEntityAsync(
selectedRestEntity.Name, existingAssociation.DestinationId, restData);
if (updateResult != null)
{
updatedCount++;
transferResult.Status = "updated";
transferResult.Message = $"Record aggiornato con successo tramite associazione (ID: {existingAssociation.DestinationId})";
transferResult.EntityId = existingAssociation.DestinationId;
// Aggiorna l'associazione con la data di ultimo aggiornamento e verifica
existingAssociation.UpdatedAt = DateTime.UtcNow;
existingAssociation.LastVerifiedAt = DateTime.UtcNow;
await CredentialService.UpdateKeyAssociationAsync(existingAssociation);
Logger.LogInformation("ASSOCIATION DEBUG: Record aggiornato con successo tramite associazione: {EntityId} per valore chiave {KeyValue}",
existingAssociation.DestinationId, sourceKey);
transferResults.Add(transferResult);
recordNumber++;
continue;
}
else
{
// Update fallito ma senza eccezione - probabilmente l'entità non esiste più
Logger.LogWarning("ASSOCIATION DEBUG: Aggiornamento fallito (result null) per associazione {AssociationId} - elimino associazione e creo nuovo record", existingAssociation.Id);
goto HandleInvalidAssociation;
}
}
catch (Exception updateEx)
{
// Update fallito con eccezione - probabilmente l'entità non esiste più
Logger.LogWarning(updateEx, "ASSOCIATION DEBUG: Aggiornamento fallito per associazione {AssociationId} - elimino associazione e creo nuovo record", existingAssociation.Id);
goto HandleInvalidAssociation;
}
HandleInvalidAssociation:
// L'ID di destinazione non esiste più o l'update è fallito - elimina l'associazione non valida
try
{
await CredentialService.DeleteKeyAssociationAsync(existingAssociation.Id);
Logger.LogInformation("ASSOCIATION DEBUG: Associazione non valida eliminata: {AssociationId}", existingAssociation.Id);
}
catch (Exception delEx)
{
Logger.LogWarning(delEx, "Errore nell'eliminazione dell'associazione non valida {AssociationId}", existingAssociation.Id);
}
transferResult.Status = "info";
transferResult.Message = $"Associazione non valida eliminata (aggiornamento fallito) - creazione nuovo record";
// Procedi con la creazione di un nuovo record (non aggiungere il result qui, sarà aggiunto dopo CreateNewRecord)
}
}
CreateNewRecord:
// Crea un nuovo record
var result = await currentRestClient.CreateEntityAsync(selectedRestEntity.Name, restData);
if (result != null)
{
successCount++;
transferResult.Status = "success";
transferResult.Message = "Record inserito con successo";
transferResult.EntityId = result.ContainsKey("id") ? result["id"]?.ToString() :
result.ContainsKey("Id") ? result["Id"]?.ToString() :
result.ContainsKey("DocEntry") ? result["DocEntry"]?.ToString() : null;
// Crea associazione solo se abbiamo una chiave sorgente e un ID destinazione
if (useRecordAssociations && !string.IsNullOrEmpty(sourceKey) && !string.IsNullOrEmpty(transferResult.EntityId))
{
try
{
// Determina i campi chiave automaticamente
var destinationKeyField = GetEntityIdField(); // Campo chiave nella destinazione
var association = new KeyAssociation
{
KeyValue = sourceKey,
SourceKeyField = sourceKeyField,
DestinationKeyField = destinationKeyField,
DestinationEntity = selectedRestEntity.Name,
DestinationId = transferResult.EntityId,
RestCredentialName = selectedRestCredential,
CreatedAt = DateTime.UtcNow,
LastVerifiedAt = DateTime.UtcNow,
AdditionalInfo = System.Text.Json.JsonSerializer.Serialize(new
{
TransferDate = DateTime.UtcNow,
RecordNumber = recordNumber,
MappingCount = fieldMappings.Count,
SourceType = selectedSourceType
})
};
Logger.LogInformation("ASSOCIATION DEBUG: Creazione nuova associazione - KeyValue: '{KeyValue}', Entity: '{Entity}', DestinationId: '{DestinationId}', Credential: '{Credential}'",
sourceKey, selectedRestEntity.Name, transferResult.EntityId, selectedRestCredential);
var associationId = await CredentialService.SaveKeyAssociationAsync(association);
Logger.LogInformation("DEBUG: Associazione salvata con ID: {AssociationId}", associationId);
}
catch (Exception assocEx)
{
Logger.LogWarning(assocEx, "Errore nella creazione dell'associazione per record {RecordNumber}", recordNumber);
// Non interrompiamo il trasferimento per errori di associazione
}
}
Logger.LogDebug("Record trasferito con successo: {Data}", string.Join(", ", restData.Select(kvp => $"{kvp.Key}={kvp.Value}")));
}
else
{
errorCount++;
transferResult.Status = "error";
transferResult.Message = "Errore nel trasferimento del record (result null)";
errors.Add($"Errore nel trasferimento del record {recordNumber}");
}
}
catch (Exception ex)
{
errorCount++;
transferResult.Status = "error";
transferResult.Message = $"Errore: {ex.Message}";
errors.Add($"Errore nel trasferimento del record {recordNumber}: {ex.Message}");
Logger.LogError(ex, "Errore nel trasferimento del record {RecordNumber}", recordNumber);
}
transferResults.Add(transferResult);
recordNumber++;
}
// 4. Mostra risultati
if (errorCount == 0)
{
var message = $"Trasferimento completato con successo! ";
var messageParts = new List<string>();
if (successCount > 0) messageParts.Add($"{successCount} record inseriti");
if (updatedCount > 0) messageParts.Add($"{updatedCount} record aggiornati");
if (duplicateCount > 0) messageParts.Add($"{duplicateCount} duplicati rilevati (warning)");
message += string.Join(", ", messageParts) + ".";
transferMessage = message;
transferMessageType = "success";
}
else
{
var message = $"Trasferimento completato con {(duplicateCount > 0 ? "warning e " : "")}errori. ";
var messageParts = new List<string>();
if (successCount > 0) messageParts.Add($"Inserimenti: {successCount}");
if (updatedCount > 0) messageParts.Add($"Aggiornamenti: {updatedCount}");
if (duplicateCount > 0) messageParts.Add($"Duplicati (warning): {duplicateCount}");
messageParts.Add($"Errori: {errorCount}");
message += string.Join(", ", messageParts);
if (errors.Any())
{
message += $". Primi errori: {string.Join("; ", errors.Take(3))}";
}
transferMessage = message;
transferMessageType = errorCount > 0 ? "error" : "warning";
}
Logger.LogInformation("Trasferimento completato. Inserimenti: {SuccessCount}, Aggiornamenti: {UpdatedCount}, Duplicati: {DuplicateCount}, Errori: {ErrorCount}",
successCount, updatedCount, duplicateCount, errorCount);
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore generale nel trasferimento dati");
transferMessage = $"Errore nel trasferimento dati: {ex.Message}";
transferMessageType = "error";
}
finally
{
isTransferringData = false;
}
} private async Task<IEnumerable<Dictionary<string, object>>> GetAllRecordsFromSource()
{
if (selectedSourceType == "database")
{
return await GetAllRecordsFromDatabase();
}
else if (selectedSourceType == "file")
{
return await GetAllRecordsFromFile();
}
return new List<Dictionary<string, object>>();
}
private async Task<IEnumerable<Dictionary<string, object>>> GetAllRecordsFromDatabase()
{
if (currentDatabaseManager == null)
return new List<Dictionary<string, object>>();
try
{
if (useCustomQuery)
{
// Usa la query custom per ottenere tutti i record
if (!isQueryValid || string.IsNullOrWhiteSpace(customQuery))
{
throw new InvalidOperationException("Query custom non valida. Validare la query prima di procedere.");
}
// CONTROLLO DI SICUREZZA AGGIUNTIVO: Verifica che sia ancora una SELECT
if (!IsSelectQuery(customQuery))
{
throw new InvalidOperationException("ERRORE DI SICUREZZA: Tentativo di eseguire una query non SELECT. Operazione bloccata per sicurezza.");
}
var cleanQuery = CleanQuery(customQuery);
Logger.LogInformation("Esecuzione query custom per trasferimento dati: {Query}", cleanQuery);
return await currentDatabaseManager.ExecuteRawQueryAsync(cleanQuery);
}
else
{
// Usa il metodo standard per tabelle
if (string.IsNullOrEmpty(selectedTable))
{
throw new InvalidOperationException("Nessuna tabella selezionata.");
}
return await currentDatabaseManager.GetAllRecordsAsync(selectedTable);
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nell'ottenere i record dal database. UseCustomQuery: {UseCustomQuery}, Table: {Table}, Query: {Query}",
useCustomQuery, selectedTable, useCustomQuery ? customQuery : "N/A");
throw;
}
} private async Task<IEnumerable<Dictionary<string, object>>> GetAllRecordsFromFile()
{
if (string.IsNullOrEmpty(selectedSheet) || !fileData.ContainsKey(selectedSheet))
{
return new List<Dictionary<string, object>>();
}
await Task.CompletedTask;
return fileData[selectedSheet];
}
private Dictionary<string, object> TransformRecordToRestEntity(Dictionary<string, object> dbRecord)
{
var restData = new Dictionary<string, object>();
foreach (var mapping in fieldMappings)
{
string dbColumn = mapping.Key;
string restProperty = mapping.Value;
if (dbRecord.ContainsKey(dbColumn))
{
var value = dbRecord[dbColumn];
// Trasforma il valore se necessario (es. date format, null handling, etc.)
var transformedValue = TransformValue(value, dbColumn, restProperty);
if (transformedValue != null)
{
restData[restProperty] = transformedValue;
}
}
}
Logger.LogDebug("Record trasformato: {DbColumns} → {RestProperties}",
string.Join(", ", dbRecord.Keys),
string.Join(", ", restData.Keys));
return restData;
}
private object? TransformValue(object? value, string dbColumn, string restProperty)
{
if (value == null || value == DBNull.Value)
return null;
// Ottieni informazioni sui tipi per fare trasformazioni intelligenti
var dbColumnInfo = databaseTables.ContainsKey(selectedTable)
? databaseTables[selectedTable].FirstOrDefault(c => c.Name == dbColumn)
: null;
var restPropertyInfo = restEntityDetails?.Properties.FirstOrDefault(p => p.Name == restProperty);
// Trasformazioni specifiche per tipo
if (restPropertyInfo != null)
{
switch (restPropertyInfo.Type.ToLower())
{
case "edm.string":
return value.ToString();
case "edm.int32":
case "edm.int64":
if (int.TryParse(value.ToString(), out int intVal))
return intVal;
break;
case "edm.decimal":
case "edm.double":
if (decimal.TryParse(value.ToString(), out decimal decVal))
return decVal;
break;
case "edm.boolean":
if (bool.TryParse(value.ToString(), out bool boolVal))
return boolVal;
// Gestisci anche valori numerici (0/1) come boolean
if (value.ToString() == "1") return true;
if (value.ToString() == "0") return false;
break;
case "edm.datetime":
case "edm.datetimeoffset":
if (DateTime.TryParse(value.ToString(), out DateTime dateVal))
return dateVal.ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
break;
}
}
// Fallback: restituisci il valore convertito a stringa
return value.ToString();
}
private string GetPropertyPlaceholder(RestPropertyInfo property)
{
return property.Type switch
{
"Edm.String" => $"Inserisci {property.Name}" + (property.MaxLength.HasValue ? $" (max {property.MaxLength})" : ""),
"Edm.Int32" => "Numero intero",
"Edm.Decimal" => "Numero decimale",
"Edm.DateTime" => "Data/Ora (YYYY-MM-DD)",
"Edm.Boolean" => "true/false",
_ => $"Valore per {property.Name}"
};
}
public void Dispose()
{
currentDatabaseManager?.Dispose();
}
private char DetectCsvSeparator(string line)
{
// Common separators to check
var separators = new[] { ',', ';', '\t', '|' };
var counts = new Dictionary<char, int>();
bool inQuotes = false;
// Count separators outside of quotes
foreach (char c in line)
{
if (c == '"')
{
inQuotes = !inQuotes;
}
else if (!inQuotes && separators.Contains(c))
{
counts[c] = counts.GetValueOrDefault(c, 0) + 1;
}
}
// Return the separator with the highest count, default to comma
if (counts.Any())
{
var mostCommon = counts.OrderByDescending(x => x.Value).First();
// Make sure we have at least one occurrence to avoid single-column files
if (mostCommon.Value > 0)
{
return mostCommon.Key;
}
}
return ','; // Default fallback
}
/// <summary>
/// Verifica se il pulsante di trasferimento può essere abilitato
/// </summary>
private bool IsTransferButtonEnabled()
{
// Base requirements
if (!fieldMappings.Any())
return false;
// Se il sistema di associazioni è abilitato, il campo chiave sorgente è obbligatorio
if (useRecordAssociations && string.IsNullOrEmpty(sourceKeyField))
return false;
return true;
}
// Helper methods per UI risultati
private string GetResultRowClass(string status)
{
return status switch
{
"success" => "",
"updated" => "table-info",
"duplicate" => "table-warning",
"error" => "table-danger",
_ => ""
};
}
private string GetResultBadgeClass(string status)
{
return status switch
{
"success" => "bg-success",
"updated" => "bg-info",
"duplicate" => "bg-warning text-dark",
"error" => "bg-danger",
_ => "bg-secondary"
};
}
private string GetResultIcon(string status)
{
return status switch
{
"success" => "fa-check-circle",
"updated" => "fa-edit",
"duplicate" => "fa-exclamation-triangle",
"error" => "fa-times-circle",
_ => "fa-question-circle"
};
}
private string GetResultStatusText(string status)
{
return status switch
{
"success" => "Inserito",
"updated" => "Aggiornato",
"duplicate" => "Duplicato",
"error" => "Errore",
_ => "Sconosciuto"
};
}
/// <summary>
/// Genera una chiave univoca per il record sorgente
/// </summary>
private string GenerateSourceKey(Dictionary<string, object> record)
{
try
{
// Il campo chiave sorgente deve essere sempre specificato
if (string.IsNullOrEmpty(sourceKeyField))
{
throw new InvalidOperationException("Campo chiave sorgente non specificato. La selezione del campo chiave è obbligatoria.");
}
if (!record.ContainsKey(sourceKeyField))
{
throw new InvalidOperationException($"Il campo chiave '{sourceKeyField}' non è presente nel record sorgente.");
}
var keyValue = record[sourceKeyField]?.ToString();
if (string.IsNullOrEmpty(keyValue))
{
throw new InvalidOperationException($"Il valore del campo chiave '{sourceKeyField}' è vuoto o null per questo record.");
}
// Normalizza il valore della chiave (trim e gestione case-sensitive)
return keyValue.Trim();
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nella generazione della chiave sorgente per il campo {SourceKeyField}", sourceKeyField);
throw;
}
}
private async Task HandleDatabaseSelectionRequired()
{
try
{
if (currentDatabaseManager == null)
{
databaseErrorMessage = "Database manager non inizializzato";
return;
}
// Ottieni la lista dei database disponibili
availableDatabases = await currentDatabaseManager.GetAvailableDatabasesAsync();
if (availableDatabases != null && availableDatabases.Any())
{
// Mostra il modal per la selezione del database
showDatabaseSelectionModal = true;
StateHasChanged();
}
else
{
databaseErrorMessage = "Nessun database disponibile per la selezione";
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nell'ottenere la lista dei database disponibili");
databaseErrorMessage = $"Errore nel recupero dei database: {ex.Message}";
}
}
private async Task OnDatabaseSelected()
{
if (string.IsNullOrEmpty(selectedDatabase))
{
return;
}
if (currentDatabaseManager == null)
{
databaseErrorMessage = "Database manager non inizializzato";
return;
}
try
{
// Cambia il database attivo
await currentDatabaseManager.ChangeDatabaseAsync(selectedDatabase);
// Nasconde il modal
showDatabaseSelectionModal = false;
// Ritenta il discovery dello schema
var schema = await currentDatabaseManager.GetDatabaseSchemaAsync();
databaseTables = schema as Dictionary<string, IEnumerable<DbColumnInfo>> ??
(schema != null ? new Dictionary<string, IEnumerable<DbColumnInfo>>(schema) : new Dictionary<string, IEnumerable<DbColumnInfo>>());
if (databaseTables.Count == 0)
{
databaseErrorMessage = $"Il database '{selectedDatabase}' non contiene tabelle accessibili";
}
else
{
isDatabaseConnected = true;
databaseErrorMessage = "";
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nel cambio di database a {Database}", selectedDatabase);
databaseErrorMessage = $"Errore nel cambio di database: {ex.Message}";
}
finally
{
StateHasChanged();
}
}
private void CancelDatabaseSelection()
{
showDatabaseSelectionModal = false;
selectedDatabase = "";
StateHasChanged();
}
/// <summary>
/// Ottiene il nome del campo ID per l'entità corrente
/// </summary>
private string GetEntityIdField()
{
// Fallback predefiniti in base al tipo di servizio/entità
if (selectedRestEntity?.Name != null)
{
// Per SAP B1, la maggior parte delle entità usa DocEntry
if (selectedRestEntity.Name.Contains("BusinessPartner") ||
selectedRestEntity.Name.Contains("Customer") ||
selectedRestEntity.Name.Contains("Vendor"))
{
return "CardCode";
}
if (selectedRestEntity.Name.Contains("Item") ||
selectedRestEntity.Name.Contains("Product"))
{
return "ItemCode";
}
}
// Usa campi ID comuni come fallback
var commonIdFields = new[] { "DocEntry", "Id", "ID", "id", "Key", "key", "Code", "code" };
// Per ora usa DocEntry come default per SAP B1
return "DocEntry";
}
// Custom Query Methods
private void OnQueryModeChanged(ChangeEventArgs e)
{
useCustomQuery = bool.Parse(e.Value?.ToString() ?? "false");
if (useCustomQuery)
{
// Reset table selection when switching to custom query
selectedTable = "";
ClearAllMappings();
// Reset query-specific state
customQuery = "";
isQueryValid = false;
queryValidationMessage = "";
queryPreviewData.Clear();
queryColumns.Clear();
showQueryPreview = false;
// For custom queries, require manual key selection
sourceKeyField = "";
suggestedPrimaryKey = "";
requiresManualKeySelection = true;
}
else
{
// Reset custom query when switching to table mode
customQuery = "";
isQueryValid = false;
queryValidationMessage = "";
queryPreviewData.Clear();
queryColumns.Clear();
showQueryPreview = false;
ClearAllMappings();
// Reset key field selection
sourceKeyField = "";
suggestedPrimaryKey = "";
requiresManualKeySelection = false;
}
StateHasChanged();
}
private async Task ValidateCustomQuery()
{
if (string.IsNullOrWhiteSpace(customQuery) || currentDatabaseManager == null)
{
isQueryValid = false;
queryValidationMessage = "Query vuota o database non connesso";
return;
}
// CONTROLLO DI SICUREZZA: Verifica che sia una SELECT
if (!IsSelectQuery(customQuery))
{
isQueryValid = false;
queryValidationMessage = "ERRORE DI SICUREZZA: Sono permesse solo query SELECT. Operazioni come INSERT, UPDATE, DELETE, DROP, CREATE, ALTER, TRUNCATE non sono consentite.";
Logger.LogWarning("Tentativo di eseguire query non SELECT bloccato: {Query}", customQuery.Length > 100 ? customQuery.Substring(0, 100) + "..." : customQuery);
return;
}
isValidatingQuery = true;
queryValidationMessage = "";
queryColumns.Clear();
try
{
// Converte la query per testare solo 1 riga
var testQuery = ConvertQueryForValidation(customQuery);
Logger.LogInformation("Validazione query: {TestQuery}", testQuery);
// Esegue la query di test
var testResults = await currentDatabaseManager.ExecuteRawQueryAsync(testQuery);
if (testResults != null && testResults.Any())
{
isQueryValid = true;
// Estrae i nomi delle colonne dal primo record
var firstRecord = testResults.First();
queryColumns = firstRecord.Keys.ToList();
// Non mostra più messaggi di successo per ridurre l'ingombro visivo
queryValidationMessage = "";
Logger.LogInformation("Query validata con successo. Colonne: {Columns}", string.Join(", ", queryColumns));
// Clear existing mappings since we have new columns
fieldMappings.Clear();
selectedDbColumn = "";
selectedRestProperty = "";
// For custom queries, always require manual key selection
sourceKeyField = "";
suggestedPrimaryKey = "";
requiresManualKeySelection = true;
StateHasChanged();
}
else
{
isQueryValid = false;
queryValidationMessage = "Query valida ma non restituisce risultati";
queryColumns.Clear();
}
}
catch (Exception ex)
{
isQueryValid = false;
queryValidationMessage = $"Errore nella validazione: {ex.Message}";
queryColumns.Clear();
Logger.LogError(ex, "Errore nella validazione della query custom");
}
finally
{
isValidatingQuery = false;
StateHasChanged();
}
}
private async Task LoadQueryPreview()
{
if (!isQueryValid || string.IsNullOrWhiteSpace(customQuery) || currentDatabaseManager == null)
{
return;
}
isLoadingPreview = true;
try
{
// Usa la query limitata per il preview (max 50 righe per performance)
var previewQuery = ConvertQueryForPreview(customQuery, 50);
Logger.LogInformation("Caricamento preview query: {PreviewQuery}", previewQuery);
queryPreviewData = await currentDatabaseManager.ExecuteRawQueryAsync(previewQuery);
showQueryPreview = true;
Logger.LogInformation("Preview caricato: {RowCount} righe", queryPreviewData.Count);
}
catch (Exception ex)
{
queryValidationMessage = $"Errore nel caricamento preview: {ex.Message}";
Logger.LogError(ex, "Errore nel caricamento del preview della query");
}
finally
{
isLoadingPreview = false;
StateHasChanged();
}
}
private void HideQueryPreview()
{
showQueryPreview = false;
queryPreviewData.Clear();
StateHasChanged();
}
private string ConvertQueryForValidation(string originalQuery)
{
// Rimuove commenti e spazi extra
var cleanQuery = CleanQuery(originalQuery);
// Se la query ha già un LIMIT/TOP, la usa così com'è per il test
if (HasLimitClause(cleanQuery))
{
return cleanQuery;
}
// Aggiunge LIMIT/TOP in base al tipo di database
return AddLimitClause(cleanQuery, 1);
}
private string ConvertQueryForPreview(string originalQuery, int maxRows = 50)
{
var cleanQuery = CleanQuery(originalQuery);
// Se la query ha già un LIMIT/TOP con un valore minore, la mantiene
if (HasLimitClause(cleanQuery))
{
return cleanQuery;
}
return AddLimitClause(cleanQuery, maxRows);
}
private string CleanQuery(string query)
{
if (string.IsNullOrWhiteSpace(query))
return "";
// Rimuove commenti SQL
var lines = query.Split('\n')
.Select(line => line.Contains("--") ? line.Substring(0, line.IndexOf("--")) : line)
.Where(line => !string.IsNullOrWhiteSpace(line));
var cleanQuery = string.Join(" ", lines).Trim();
// Rimuove il punto e virgola finale se presente
if (cleanQuery.EndsWith(";"))
{
cleanQuery = cleanQuery.Substring(0, cleanQuery.Length - 1);
}
return cleanQuery;
}
private bool HasLimitClause(string query)
{
var upperQuery = query.ToUpperInvariant();
return upperQuery.Contains(" LIMIT ") ||
upperQuery.Contains(" TOP ") ||
upperQuery.Contains("ROWNUM") ||
upperQuery.Contains("FETCH FIRST");
}
private string AddLimitClause(string query, int limit)
{
var upperQuery = query.ToUpperInvariant();
// Per SQL Server, Oracle, e altri che supportano TOP
if (upperQuery.Contains("SELECT "))
{
var credential = databaseCredentials.FirstOrDefault(c => c.Name == selectedDatabaseCredential);
if (credential != null)
{
var dbType = credential.DatabaseType.ToString().ToLowerInvariant();
switch (dbType)
{
case "sqlserver":
case "oracle":
// Aggiunge TOP dopo SELECT
return query.Replace("SELECT ", $"SELECT TOP {limit} ", StringComparison.OrdinalIgnoreCase);
case "mysql":
case "postgresql":
case "sqlite":
default:
// Aggiunge LIMIT alla fine
return $"{query} LIMIT {limit}";
}
}
}
// Fallback: aggiunge LIMIT
return $"{query} LIMIT {limit}";
}
private void OnCustomQueryChanged(ChangeEventArgs e)
{
customQuery = e.Value?.ToString() ?? "";
// Reset validation quando la query cambia
isQueryValid = false;
queryValidationMessage = "";
queryPreviewData.Clear();
queryColumns.Clear();
showQueryPreview = false;
// Clear mappings quando la query cambia
ClearAllMappings();
// Reset key field selection
sourceKeyField = "";
suggestedPrimaryKey = "";
requiresManualKeySelection = true;
StateHasChanged();
}
/// <summary>
/// Verifica che la query sia una SELECT e non contenga operazioni pericolose
/// </summary>
private bool IsSelectQuery(string query)
{
if (string.IsNullOrWhiteSpace(query))
return false;
// Rimuovi commenti e normalizza la query
var cleanQuery = CleanQueryForSecurityCheck(query);
// Lista delle operazioni pericolose che non sono permesse
var dangerousKeywords = new[]
{
"INSERT", "UPDATE", "DELETE", "DROP", "CREATE", "ALTER",
"TRUNCATE", "REPLACE", "MERGE", "EXEC", "EXECUTE",
"DECLARE", "SET", "GRANT", "REVOKE", "BACKUP", "RESTORE",
"SHUTDOWN", "KILL", "LOAD", "BULK", "OPENROWSET", "OPENDATASOURCE"
};
// Verifica che non contenga operazioni pericolose
foreach (var keyword in dangerousKeywords)
{
if (cleanQuery.Contains($" {keyword} ", StringComparison.OrdinalIgnoreCase) ||
cleanQuery.StartsWith($"{keyword} ", StringComparison.OrdinalIgnoreCase) ||
cleanQuery.Contains($";{keyword} ", StringComparison.OrdinalIgnoreCase) ||
cleanQuery.Contains($"\n{keyword} ", StringComparison.OrdinalIgnoreCase) ||
cleanQuery.Contains($"\r{keyword} ", StringComparison.OrdinalIgnoreCase))
{
Logger.LogWarning("Query bloccata: contiene keyword pericolosa '{Keyword}' in query: {QueryStart}",
keyword, query.Length > 50 ? query.Substring(0, 50) + "..." : query);
return false;
}
}
// Verifica che inizi con SELECT (permettendo spazi e commenti iniziali)
var trimmedQuery = cleanQuery.TrimStart();
if (!trimmedQuery.StartsWith("SELECT", StringComparison.OrdinalIgnoreCase))
{
Logger.LogWarning("Query bloccata: non inizia con SELECT. Query: {QueryStart}",
query.Length > 50 ? query.Substring(0, 50) + "..." : query);
return false;
}
// Verifica addizionale: non deve contenere punto e virgola seguito da altra query
var statements = cleanQuery.Split(';', StringSplitOptions.RemoveEmptyEntries);
if (statements.Length > 1)
{
// Se ci sono multiple statements, tutte devono essere SELECT o commenti vuoti
foreach (var statement in statements)
{
var trimmedStatement = statement.Trim();
if (!string.IsNullOrEmpty(trimmedStatement) &&
!trimmedStatement.StartsWith("SELECT", StringComparison.OrdinalIgnoreCase))
{
Logger.LogWarning("Query bloccata: contiene multiple statements non SELECT. Query: {QueryStart}",
query.Length > 50 ? query.Substring(0, 50) + "..." : query);
return false;
}
}
}
return true;
}
/// <summary>
/// Pulisce la query per il controllo di sicurezza rimuovendo commenti
/// </summary>
private string CleanQueryForSecurityCheck(string query)
{
if (string.IsNullOrEmpty(query))
return "";
var lines = query.Split('\n');
var cleanedLines = new List<string>();
foreach (var line in lines)
{
var cleanedLine = line.Trim();
// Rimuovi commenti SQL (-- e /* */)
var dashCommentIndex = cleanedLine.IndexOf("--");
if (dashCommentIndex >= 0)
{
cleanedLine = cleanedLine.Substring(0, dashCommentIndex).Trim();
}
// Gestione commenti multiline /* */ - implementazione base
cleanedLine = System.Text.RegularExpressions.Regex.Replace(cleanedLine, @"/\*.*?\*/", " ",
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
if (!string.IsNullOrWhiteSpace(cleanedLine))
{
cleanedLines.Add(cleanedLine);
}
}
return string.Join(" ", cleanedLines);
}
}