ee3c251b08
- Aggiunto campo SourceCustomQuery al modello DataCouplerProfile e DTO - Creata migrazione database per la nuova colonna SourceCustomQuery - Aggiornato DataCouplerProfileService per gestire il mapping della query custom - Modificato ProfileSaver per includere la query custom nel salvataggio - Implementata logica di caricamento profili con supporto query custom: * Popolamento automatico della textbox con query salvata * Validazione ed esecuzione automatica della query al caricamento * Caricamento anteprima dati e mapping dopo validazione query * Gestione priorità: query custom ha precedenza sulla selezione tabella - Aggiornato DataCoupler.razor per passare la query custom al ProfileSaver - Corretto salvataggio profili esistenti per includere SourceCustomQuery Il sistema ora permette di salvare e ripristinare completamente le configurazioni con query SQL personalizzate, mantenendo il comportamento esistente per le
3354 lines
134 KiB
C#
3354 lines
134 KiB
C#
using System;
|
|
using System.Data;
|
|
using System.Text;
|
|
using CredentialManager.Models;
|
|
using CredentialManager.Services;
|
|
using DataConnection.Interfaces;
|
|
using DataConnection.REST.Interfaces;
|
|
using DataConnection.REST.Models;
|
|
using DataConnection.CredentialManagement.Interfaces;
|
|
using ExcelDataReader;
|
|
using Microsoft.AspNetCore.Components;
|
|
using Microsoft.AspNetCore.Components.Forms;
|
|
using Microsoft.JSInterop;
|
|
using Microsoft.Extensions.Logging;
|
|
using Data_Coupler.Services;
|
|
|
|
namespace Data_Coupler.Pages;
|
|
|
|
public partial class DataCoupler : ComponentBase
|
|
{
|
|
// Proprietà iniettate
|
|
[Inject] public IDataConnectionCredentialService CredentialService { get; set; } = default!;
|
|
[Inject] public IDataConnectionFactory ConnectionFactory { get; set; } = default!;
|
|
[Inject] public IJSRuntime JSRuntime { get; set; } = default!;
|
|
[Inject] public ILogger<DataCoupler> Logger { get; set; } = default!;
|
|
[Inject] public IDataCouplerProfileService ProfileService { get; set; } = default!;
|
|
|
|
// 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 List<string> availableTableNames = new(); // Solo nomi delle tabelle
|
|
private Dictionary<string, IEnumerable<DbColumnInfo>> databaseTables = new(); // Schema dettagliato per tabelle caricate
|
|
private string selectedTable = "";
|
|
private string databaseSearchTerm = "";
|
|
|
|
// Database selection - per gestire la selezione del database quando non specificato nella connection string
|
|
private List<string> availableDatabases = new();
|
|
private string selectedDatabase = "";
|
|
private bool showDatabaseSelectionModal = false;
|
|
private bool isLoadingDatabases = false;
|
|
|
|
// Database selection (schemas only)
|
|
private List<string> availableSchemas = new();
|
|
private string selectedSchema = "";
|
|
private bool showSchemaSelectionModal = false;
|
|
private bool isLoadingSchemas = 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 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;
|
|
|
|
// Gestione Profili
|
|
private List<DataCouplerProfile> availableProfiles = new();
|
|
private bool isLoadingProfiles = false;
|
|
private bool showProfileManagement = false;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
await LoadCredentials();
|
|
}
|
|
private async Task LoadCredentials()
|
|
{
|
|
try
|
|
{
|
|
databaseCredentials = await CredentialService.GetAllDatabaseCredentialsAsync();
|
|
restApiCredentials = await CredentialService.GetAllRestApiCredentialsAsync();
|
|
// Carica anche i profili disponibili
|
|
await LoadProfiles();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Errore nel caricamento delle credenziali");
|
|
await JSRuntime.InvokeVoidAsync("alert", $"Errore nel caricamento delle credenziali: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private async Task LoadProfiles()
|
|
{
|
|
try
|
|
{
|
|
isLoadingProfiles = true;
|
|
var profiles = await ProfileService.GetAllProfilesAsync();
|
|
availableProfiles = profiles.ToList();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Errore nel caricamento dei profili");
|
|
}
|
|
finally
|
|
{
|
|
isLoadingProfiles = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
private async Task OnProfileLoaded(DataCouplerProfile profile)
|
|
{
|
|
try
|
|
{
|
|
// Aggiorna la data di ultimo utilizzo
|
|
await ProfileService.UpdateLastUsedAsync(profile.Id);
|
|
|
|
// Applica la configurazione del profilo
|
|
await ApplyProfileConfiguration(profile);
|
|
|
|
// Ricarica i profili per aggiornare la data di ultimo utilizzo
|
|
await LoadProfiles();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Errore nel caricamento del profilo");
|
|
await JSRuntime.InvokeVoidAsync("alert", $"Errore nel caricamento del profilo: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private async Task ApplyProfileConfiguration(DataCouplerProfile profile)
|
|
{
|
|
Logger.LogInformation("=== INIZIO APPLICAZIONE PROFILO ===");
|
|
Logger.LogInformation("Applicando configurazione profilo: {ProfileName}", profile.Name);
|
|
Logger.LogInformation("Profilo - SourceType: {SourceType}, SourceCredentialId: {SourceCredentialId}, DestinationCredentialId: {DestinationCredentialId}",
|
|
profile.SourceType, profile.SourceCredentialId, profile.DestinationCredentialId);
|
|
|
|
try
|
|
{
|
|
// Step 0: Log dello stato iniziale
|
|
Logger.LogInformation("Stato iniziale - SelectedSourceType: {SourceType}, DatabaseConnected: {DatabaseConnected}, RestConnected: {RestConnected}",
|
|
selectedSourceType, isDatabaseConnected, isRestConnected);
|
|
|
|
// Reset dello stato corrente
|
|
Logger.LogInformation("Resettando stato corrente...");
|
|
ResetAllState();
|
|
Logger.LogInformation("Stato dopo reset - SelectedSourceType: {SourceType}, DatabaseConnected: {DatabaseConnected}, RestConnected: {RestConnected}",
|
|
selectedSourceType, isDatabaseConnected, isRestConnected);
|
|
|
|
// Step 1: Applica configurazione sorgente
|
|
selectedSourceType = profile.SourceType;
|
|
Logger.LogInformation("Step 1 - Tipo sorgente impostato: {SourceType}", selectedSourceType);
|
|
|
|
// Force UI update for source type change
|
|
StateHasChanged();
|
|
await Task.Delay(100); // Give UI time to react to source type change
|
|
|
|
// Step 2: Configura e connetti la sorgente
|
|
if (profile.SourceCredentialId.HasValue)
|
|
{
|
|
Logger.LogInformation("Step 2 - Configurazione sorgente con ID credenziale: {CredentialId}", profile.SourceCredentialId);
|
|
|
|
if (profile.SourceType == "database")
|
|
{
|
|
var sourceCredential = await CredentialService.GetDatabaseCredentialAsync(profile.SourceCredentialId.Value);
|
|
if (sourceCredential != null)
|
|
{
|
|
selectedDatabaseCredential = sourceCredential.Name;
|
|
Logger.LogInformation("Credenziale database selezionata: {Credential}", selectedDatabaseCredential);
|
|
|
|
// Force UI update for credential selection
|
|
StateHasChanged();
|
|
await Task.Delay(200);
|
|
|
|
// Connetti al database
|
|
Logger.LogInformation("Iniziando connessione database...");
|
|
|
|
// Gestione connessione con database specifico
|
|
if (!string.IsNullOrEmpty(profile.SourceDatabaseName))
|
|
{
|
|
Logger.LogInformation("Connessione con database specifico: {Database}", profile.SourceDatabaseName);
|
|
await ConnectToDatabaseWithSpecificDatabase(profile.SourceDatabaseName);
|
|
}
|
|
else if (!string.IsNullOrEmpty(profile.SourceSchema))
|
|
{
|
|
Logger.LogInformation("Connessione con schema specifico: {Schema}", profile.SourceSchema);
|
|
await ConnectToDatabaseWithSchema(profile.SourceSchema);
|
|
}
|
|
else
|
|
{
|
|
Logger.LogInformation("Connessione senza database/schema specifico");
|
|
await ConnectToDatabase();
|
|
}
|
|
|
|
Logger.LogInformation("Stato dopo connessione database - Connected: {Connected}, Tables: {TableCount}",
|
|
isDatabaseConnected, availableTableNames.Count);
|
|
|
|
// Gestisci la query custom se specificata nel profilo
|
|
if (!string.IsNullOrEmpty(profile.SourceCustomQuery) && isDatabaseConnected)
|
|
{
|
|
Logger.LogInformation("Caricamento query custom dal profilo: {Query}", profile.SourceCustomQuery);
|
|
|
|
// Imposta la modalità query custom
|
|
useCustomQuery = true;
|
|
customQuery = profile.SourceCustomQuery;
|
|
|
|
// Valida ed esegui la query
|
|
await ValidateCustomQuery();
|
|
|
|
if (isQueryValid)
|
|
{
|
|
Logger.LogInformation("Query custom caricata e validata con successo");
|
|
|
|
// Carica l'anteprima dei dati
|
|
await LoadQueryPreview();
|
|
|
|
Logger.LogInformation("Anteprima dati della query custom caricata");
|
|
}
|
|
else
|
|
{
|
|
Logger.LogWarning("La query custom dal profilo non è valida: {ValidationMessage}", queryValidationMessage);
|
|
}
|
|
}
|
|
// Seleziona la tabella se specificata e se la connessione è riuscita (solo se non c'è una query custom)
|
|
else if (!string.IsNullOrEmpty(profile.SourceTable) && isDatabaseConnected)
|
|
{
|
|
Logger.LogInformation("Selezione tabella: {Table}", profile.SourceTable);
|
|
await SelectTable(profile.SourceTable);
|
|
Logger.LogInformation("Tabella selezionata: {SelectedTable}, Schema caricato: {SchemaLoaded}",
|
|
selectedTable, databaseTables.ContainsKey(profile.SourceTable));
|
|
}
|
|
else if (string.IsNullOrEmpty(profile.SourceCustomQuery) && string.IsNullOrEmpty(profile.SourceTable))
|
|
{
|
|
Logger.LogInformation("Nessuna tabella o query custom specificata nel profilo");
|
|
}
|
|
else
|
|
{
|
|
Logger.LogWarning("Impossibile selezionare tabella o caricare query custom - Table: {Table}, Query: {Query}, Connected: {Connected}",
|
|
profile.SourceTable, profile.SourceCustomQuery, isDatabaseConnected);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Logger.LogWarning("Credenziale database con ID {CredentialId} non trovata", profile.SourceCredentialId);
|
|
}
|
|
}
|
|
else if (profile.SourceType == "file")
|
|
{
|
|
// Per i file, non possiamo ricreare il file caricato, ma possiamo impostare le informazioni
|
|
if (!string.IsNullOrEmpty(profile.SourceFilePath))
|
|
{
|
|
selectedFileName = Path.GetFileName(profile.SourceFilePath);
|
|
Logger.LogInformation("Informazioni file impostate: {FileName}", selectedFileName);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Logger.LogInformation("Nessuna credenziale sorgente da configurare");
|
|
}
|
|
|
|
// Small delay to let source configuration settle
|
|
await Task.Delay(300);
|
|
|
|
// Step 3: Configura e connetti la destinazione
|
|
if (profile.DestinationCredentialId.HasValue)
|
|
{
|
|
Logger.LogInformation("Step 3 - Configurazione destinazione con ID credenziale: {CredentialId}", profile.DestinationCredentialId);
|
|
|
|
var destinationCredential = await CredentialService.GetRestApiCredentialAsync(profile.DestinationCredentialId.Value);
|
|
if (destinationCredential != null)
|
|
{
|
|
selectedRestCredential = destinationCredential.Name;
|
|
Logger.LogInformation("Credenziale REST selezionata: {Credential}", selectedRestCredential);
|
|
|
|
// Force UI update for REST credential selection
|
|
StateHasChanged();
|
|
await Task.Delay(200);
|
|
|
|
// Connetti al servizio REST
|
|
Logger.LogInformation("Iniziando connessione REST...");
|
|
await ConnectToRestApi();
|
|
|
|
Logger.LogInformation("Stato dopo connessione REST - Connected: {Connected}, Entities: {EntityCount}",
|
|
isRestConnected, restEntities.Count);
|
|
|
|
// Seleziona l'entità REST se la connessione è riuscita
|
|
if (!string.IsNullOrEmpty(profile.DestinationEndpoint) && isRestConnected)
|
|
{
|
|
var entity = restEntities.FirstOrDefault(e => e.Name == profile.DestinationEndpoint);
|
|
if (entity != null)
|
|
{
|
|
Logger.LogInformation("Selezione entità REST: {Entity}", entity.Name);
|
|
await SelectRestEntity(entity);
|
|
Logger.LogInformation("Entità REST selezionata: {SelectedEntity}, Dettagli caricati: {DetailsLoaded}",
|
|
selectedRestEntity?.Name, restEntityDetails != null);
|
|
}
|
|
else
|
|
{
|
|
Logger.LogWarning("Entità REST non trovata: {Endpoint} - Entities disponibili: {Entities}",
|
|
profile.DestinationEndpoint, string.Join(", ", restEntities.Select(e => e.Name)));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Logger.LogWarning("Impossibile selezionare entità REST - Endpoint: {Endpoint}, Connected: {Connected}",
|
|
profile.DestinationEndpoint, isRestConnected);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Logger.LogWarning("Credenziale REST con ID {CredentialId} non trovata", profile.DestinationCredentialId);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Logger.LogInformation("Nessuna credenziale destinazione da configurare");
|
|
}
|
|
|
|
// Small delay to let destination configuration settle
|
|
await Task.Delay(300);
|
|
|
|
// Step 4: Applica mapping dei campi se disponibile
|
|
if (!string.IsNullOrEmpty(profile.FieldMappingJson))
|
|
{
|
|
Logger.LogInformation("Step 4 - Applicazione mapping campi...");
|
|
try
|
|
{
|
|
var service = new DataCouplerProfileService(null!);
|
|
var mappings = service.DeserializeFieldMappings(profile.FieldMappingJson);
|
|
|
|
Logger.LogInformation("Mappings deserializzati: {Count}", mappings.Count);
|
|
|
|
// Applica i mapping
|
|
fieldMappings.Clear();
|
|
keyFields.Clear();
|
|
|
|
foreach (var mapping in mappings)
|
|
{
|
|
fieldMappings[mapping.SourceField] = mapping.DestinationField;
|
|
if (mapping.IsKey)
|
|
{
|
|
keyFields.Add(mapping.DestinationField);
|
|
}
|
|
Logger.LogInformation("Mapping applicato: {Source} -> {Destination} (IsKey: {IsKey})",
|
|
mapping.SourceField, mapping.DestinationField, mapping.IsKey);
|
|
}
|
|
|
|
Logger.LogInformation("Mappings applicati - Totale: {MappingCount}, Chiavi: {KeyCount}",
|
|
fieldMappings.Count, keyFields.Count);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogWarning(ex, "Errore nel caricamento dei mapping dei campi dal profilo");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Logger.LogInformation("Nessun mapping campi da applicare");
|
|
}
|
|
|
|
// Step 5: Applica configurazione chiave sorgente
|
|
if (!string.IsNullOrEmpty(profile.SourceKeyField))
|
|
{
|
|
sourceKeyField = profile.SourceKeyField;
|
|
Logger.LogInformation("Step 5 - Chiave sorgente applicata: {SourceKey}", sourceKeyField);
|
|
}
|
|
else
|
|
{
|
|
Logger.LogInformation("Nessuna chiave sorgente da applicare");
|
|
}
|
|
|
|
// Step 6: Applica configurazione associazioni record
|
|
useRecordAssociations = profile.UseRecordAssociations;
|
|
Logger.LogInformation("Step 6 - Associazioni record configurate: {UseAssociations}", useRecordAssociations);
|
|
|
|
Logger.LogInformation("=== FINE APPLICAZIONE PROFILO ===");
|
|
Logger.LogInformation("Stato finale - Source: {SourceType}, DatabaseConnected: {DatabaseConnected}, RestConnected: {RestConnected}, Mappings: {MappingCount}",
|
|
selectedSourceType, isDatabaseConnected, isRestConnected, fieldMappings.Count);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Errore nell'applicazione della configurazione del profilo {ProfileName}", profile.Name);
|
|
await JSRuntime.InvokeVoidAsync("alert", $"Errore nel caricamento del profilo: {ex.Message}");
|
|
}
|
|
finally
|
|
{
|
|
// Force final UI update
|
|
StateHasChanged();
|
|
Logger.LogInformation("Aggiornamento finale UI completato");
|
|
}
|
|
}
|
|
|
|
private async Task OnProfileSaved(DataCouplerProfileDto profileDto)
|
|
{
|
|
try
|
|
{
|
|
Logger.LogInformation("Tentativo di salvataggio profilo: {ProfileName}", profileDto.Name);
|
|
|
|
var profileService = new DataCouplerProfileService(null!); // Usa il service di conversione
|
|
var profile = profileService.FromDto(profileDto, "System"); // TODO: Usa utente corrente
|
|
|
|
// Controlla se esiste già un profilo con lo stesso nome (inclusi quelli inattivi)
|
|
Logger.LogInformation("Controllo esistenza profilo con nome: {ProfileName}", profileDto.Name);
|
|
var existingProfile = await ProfileService.GetProfileByNameIncludingInactiveAsync(profileDto.Name);
|
|
|
|
if (existingProfile != null)
|
|
{
|
|
Logger.LogInformation("Trovato profilo esistente con ID: {ProfileId}, IsActive: {IsActive}",
|
|
existingProfile.Id, existingProfile.IsActive);
|
|
|
|
if (!existingProfile.IsActive)
|
|
{
|
|
// Il profilo esiste ma è inattivo - riattivalo e aggiornalo
|
|
Logger.LogInformation("Riattivazione del profilo inattivo: {ProfileName}", profileDto.Name);
|
|
profile.Id = existingProfile.Id;
|
|
profile.IsActive = true;
|
|
|
|
// Aggiorna direttamente il profilo esistente invece di creare un nuovo oggetto
|
|
existingProfile.Description = profile.Description;
|
|
existingProfile.SourceType = profile.SourceType;
|
|
existingProfile.SourceCredentialId = profile.SourceCredentialId;
|
|
existingProfile.SourceSchema = profile.SourceSchema;
|
|
existingProfile.SourceTable = profile.SourceTable;
|
|
existingProfile.SourceCustomQuery = profile.SourceCustomQuery;
|
|
existingProfile.SourceFilePath = profile.SourceFilePath;
|
|
existingProfile.DestinationType = profile.DestinationType;
|
|
existingProfile.DestinationCredentialId = profile.DestinationCredentialId;
|
|
existingProfile.DestinationSchema = profile.DestinationSchema;
|
|
existingProfile.DestinationTable = profile.DestinationTable;
|
|
existingProfile.DestinationEndpoint = profile.DestinationEndpoint;
|
|
existingProfile.FieldMappingJson = profile.FieldMappingJson;
|
|
existingProfile.SourceKeyField = profile.SourceKeyField;
|
|
existingProfile.UseRecordAssociations = profile.UseRecordAssociations;
|
|
existingProfile.IsActive = true;
|
|
|
|
await ProfileService.UpdateProfileAsync(existingProfile);
|
|
await LoadProfiles();
|
|
|
|
await JSRuntime.InvokeVoidAsync("alert", $"Profilo '{profileDto.Name}' riattivato e aggiornato con successo!");
|
|
return;
|
|
}
|
|
|
|
// Il profilo esiste ed è attivo - chiedi conferma per sovrascrittura
|
|
var confirmOverwrite = await JSRuntime.InvokeAsync<bool>("confirm",
|
|
$"Esiste già un profilo attivo con il nome '{profileDto.Name}'. Vuoi sovrascriverlo?");
|
|
|
|
if (confirmOverwrite)
|
|
{
|
|
Logger.LogInformation("Utente ha confermato la sovrascrittura del profilo: {ProfileName}", profileDto.Name);
|
|
|
|
// Aggiorna il profilo esistente direttamente
|
|
existingProfile.Description = profile.Description;
|
|
existingProfile.SourceType = profile.SourceType;
|
|
existingProfile.SourceCredentialId = profile.SourceCredentialId;
|
|
existingProfile.SourceSchema = profile.SourceSchema;
|
|
existingProfile.SourceTable = profile.SourceTable;
|
|
existingProfile.SourceCustomQuery = profile.SourceCustomQuery;
|
|
existingProfile.SourceFilePath = profile.SourceFilePath;
|
|
existingProfile.DestinationType = profile.DestinationType;
|
|
existingProfile.DestinationCredentialId = profile.DestinationCredentialId;
|
|
existingProfile.DestinationSchema = profile.DestinationSchema;
|
|
existingProfile.DestinationTable = profile.DestinationTable;
|
|
existingProfile.DestinationEndpoint = profile.DestinationEndpoint;
|
|
existingProfile.FieldMappingJson = profile.FieldMappingJson;
|
|
existingProfile.SourceKeyField = profile.SourceKeyField;
|
|
existingProfile.UseRecordAssociations = profile.UseRecordAssociations;
|
|
|
|
await ProfileService.UpdateProfileAsync(existingProfile);
|
|
await LoadProfiles(); // Ricarica la lista
|
|
|
|
await JSRuntime.InvokeVoidAsync("alert", $"Profilo '{profileDto.Name}' aggiornato con successo!");
|
|
}
|
|
else
|
|
{
|
|
Logger.LogInformation("Utente ha annullato la sovrascrittura del profilo: {ProfileName}", profileDto.Name);
|
|
|
|
// Proponi di creare con un nome unico
|
|
var useUniqueName = await JSRuntime.InvokeAsync<bool>("confirm",
|
|
"Vuoi salvare il profilo con un nome unico automatico (es. 'Nome Profilo (1)')?");
|
|
|
|
if (useUniqueName)
|
|
{
|
|
var uniqueName = await GenerateUniqueProfileName(profileDto.Name);
|
|
profile.Name = uniqueName;
|
|
|
|
try
|
|
{
|
|
await ProfileService.SaveProfileAsync(profile);
|
|
await LoadProfiles();
|
|
|
|
await JSRuntime.InvokeVoidAsync("alert", $"Profilo salvato con nome '{uniqueName}'!");
|
|
}
|
|
catch (Exception uniqueEx)
|
|
{
|
|
Logger.LogError(uniqueEx, "Errore durante il salvataggio del profilo con nome unico: {UniqueName}", uniqueName);
|
|
|
|
// Gestisci l'errore di unique constraint anche per il nome unico
|
|
if (uniqueEx.Message.Contains("UNIQUE constraint failed"))
|
|
{
|
|
await JSRuntime.InvokeVoidAsync("alert",
|
|
$"Errore: Non è stato possibile generare un nome unico per il profilo. " +
|
|
"Prova a ricaricare la pagina e riprova.");
|
|
}
|
|
else
|
|
{
|
|
await JSRuntime.InvokeVoidAsync("alert", $"Errore nel salvataggio del profilo: {uniqueEx.Message}");
|
|
}
|
|
}
|
|
}
|
|
// Altrimenti, non salvare nulla
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Logger.LogInformation("Nessun profilo esistente trovato, creazione nuovo profilo: {ProfileName}", profileDto.Name);
|
|
|
|
// Crea un nuovo profilo
|
|
try
|
|
{
|
|
await ProfileService.SaveProfileAsync(profile);
|
|
await LoadProfiles(); // Ricarica la lista
|
|
|
|
await JSRuntime.InvokeVoidAsync("alert", $"Profilo '{profileDto.Name}' salvato con successo!");
|
|
}
|
|
catch (Exception saveEx)
|
|
{
|
|
Logger.LogError(saveEx, "Errore durante il salvataggio del nuovo profilo: {ProfileName}", profileDto.Name);
|
|
|
|
// Possibile race condition - riprova con controllo duplicato
|
|
if (saveEx.Message.Contains("UNIQUE constraint failed"))
|
|
{
|
|
Logger.LogWarning("Race condition rilevata durante il salvataggio, gestione del duplicato...");
|
|
|
|
// Chiedi se vuole sovrascrivere o creare nome unico
|
|
var handleDuplicate = await JSRuntime.InvokeAsync<bool>("confirm",
|
|
$"Un profilo con il nome '{profileDto.Name}' è stato creato nel frattempo. " +
|
|
"Vuoi sovrascriverlo? (Clicca 'Annulla' per salvare con un nome unico)");
|
|
|
|
if (handleDuplicate)
|
|
{
|
|
// Trova il profilo e aggiornalo
|
|
var duplicateProfile = await ProfileService.GetProfileByNameIncludingInactiveAsync(profileDto.Name);
|
|
if (duplicateProfile != null)
|
|
{
|
|
profile.Id = duplicateProfile.Id;
|
|
await ProfileService.UpdateProfileAsync(profile);
|
|
await LoadProfiles();
|
|
|
|
await JSRuntime.InvokeVoidAsync("alert", $"Profilo '{profileDto.Name}' aggiornato con successo!");
|
|
}
|
|
else
|
|
{
|
|
await JSRuntime.InvokeVoidAsync("alert", "Errore: Il profilo duplicato non è stato trovato.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Crea con nome unico
|
|
var uniqueName = await GenerateUniqueProfileName(profileDto.Name);
|
|
profile.Name = uniqueName;
|
|
|
|
await ProfileService.SaveProfileAsync(profile);
|
|
await LoadProfiles();
|
|
|
|
await JSRuntime.InvokeVoidAsync("alert", $"Profilo salvato con nome '{uniqueName}'!");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw; // Rilancia eccezioni non gestite
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Errore generale nel salvataggio del profilo: {ProfileName}", profileDto.Name);
|
|
|
|
// Gestione generica degli errori
|
|
if (ex.Message.Contains("UNIQUE constraint failed"))
|
|
{
|
|
await JSRuntime.InvokeVoidAsync("alert",
|
|
$"Errore: Esiste già un profilo con il nome '{profileDto.Name}'. " +
|
|
"Questo può accadere se ci sono stati problemi di sincronizzazione. " +
|
|
"Prova a ricaricare la pagina e riprova.");
|
|
}
|
|
else
|
|
{
|
|
await JSRuntime.InvokeVoidAsync("alert", $"Errore nel salvataggio del profilo: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task OnProfileDeleted(int profileId)
|
|
{
|
|
try
|
|
{
|
|
var deleted = await ProfileService.DeleteProfileAsync(profileId);
|
|
if (deleted)
|
|
{
|
|
await LoadProfiles(); // Ricarica la lista
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Errore nell'eliminazione del profilo");
|
|
throw; // Rilancia per gestire nell'UI
|
|
}
|
|
}
|
|
|
|
private void OnManageProfiles()
|
|
{
|
|
showProfileManagement = true;
|
|
}
|
|
|
|
private void OnCloseProfileManagement()
|
|
{
|
|
showProfileManagement = false;
|
|
}
|
|
|
|
private bool CanSaveProfile()
|
|
{
|
|
return !string.IsNullOrEmpty(selectedSourceType) &&
|
|
(!string.IsNullOrEmpty(selectedDatabaseCredential) || !string.IsNullOrEmpty(selectedRestCredential)) &&
|
|
(!string.IsNullOrEmpty(selectedRestCredential) || !string.IsNullOrEmpty(selectedTable));
|
|
}
|
|
|
|
private List<FieldMappingDto> GetCurrentFieldMappings()
|
|
{
|
|
var mappings = new List<FieldMappingDto>();
|
|
|
|
foreach (var mapping in fieldMappings)
|
|
{
|
|
mappings.Add(new FieldMappingDto
|
|
{
|
|
SourceField = mapping.Key,
|
|
DestinationField = mapping.Value,
|
|
IsKey = keyFields.Contains(mapping.Value),
|
|
IsRequired = false, // TODO: Determina dai metadati
|
|
DataType = "", // TODO: Determina dai metadati
|
|
});
|
|
}
|
|
|
|
return mappings;
|
|
}
|
|
|
|
private void ResetAllState()
|
|
{
|
|
ResetSourceState();
|
|
ResetDestinationState();
|
|
fieldMappings.Clear();
|
|
keyFields.Clear();
|
|
transferResults.Clear();
|
|
transferMessage = "";
|
|
}
|
|
|
|
private void ResetDestinationState()
|
|
{
|
|
selectedRestCredential = "";
|
|
isConnectingRest = false;
|
|
isRestConnected = false;
|
|
restErrorMessage = "";
|
|
restEntities.Clear();
|
|
selectedRestEntity = null;
|
|
restEntityDetails = null;
|
|
restSearchTerm = "";
|
|
currentRestDiscovery = null;
|
|
currentRestClient = null;
|
|
}
|
|
|
|
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, try auto-selection and then require manual key selection if not found
|
|
sourceKeyField = "";
|
|
suggestedPrimaryKey = "";
|
|
requiresManualKeySelection = true;
|
|
|
|
// AUTO-SELECT della chiave per i file
|
|
if (fileSheets.ContainsKey(sheetName))
|
|
{
|
|
var columns = fileSheets[sheetName].ToList();
|
|
TryAutoSelectKeyForFile(columns);
|
|
}
|
|
|
|
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 database selection
|
|
availableDatabases.Clear();
|
|
selectedDatabase = "";
|
|
showDatabaseSelectionModal = false;
|
|
isLoadingDatabases = false;
|
|
|
|
// 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");
|
|
|
|
// Verifica se il database è specificato nella connection string
|
|
bool isDatabaseSpecified = await IsDatabaseSpecifiedInConnectionString(credential);
|
|
|
|
if (isDatabaseSpecified)
|
|
{
|
|
Logger.LogInformation("Database specificato nella connection string. Procedendo con discovery tabelle.");
|
|
try
|
|
{
|
|
await LoadTablesFromConnectedDatabase();
|
|
isDatabaseConnected = true;
|
|
Logger.LogInformation("Tabelle caricate con successo, database connesso");
|
|
return; // Importante: usciamo qui se tutto va bene
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Errore nel caricamento tabelle dal database specificato");
|
|
databaseErrorMessage = $"Errore nel caricamento tabelle: {ex.Message}";
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Logger.LogInformation("Database non specificato nella connection string. Caricando database disponibili.");
|
|
await LoadAvailableDatabases();
|
|
|
|
if (availableDatabases.Any())
|
|
{
|
|
Logger.LogInformation("Trovati {DatabaseCount} database disponibili", availableDatabases.Count);
|
|
showDatabaseSelectionModal = true;
|
|
StateHasChanged();
|
|
return; // Non procediamo fino alla selezione del database
|
|
}
|
|
else
|
|
{
|
|
databaseErrorMessage = "Nessun database disponibile trovato";
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Errore nella connessione al database");
|
|
databaseErrorMessage = $"Errore: {ex.Message}";
|
|
}
|
|
finally
|
|
{
|
|
isConnectingDatabase = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
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 Task 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;
|
|
|
|
// Carica i dettagli della tabella se non sono già stati caricati
|
|
if (!databaseTables.ContainsKey(tableName) && currentDatabaseManager != null)
|
|
{
|
|
try
|
|
{
|
|
var tableSchema = await currentDatabaseManager.GetTableSchemaAsync(tableName);
|
|
databaseTables[tableName] = tableSchema;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Errore nel caricamento dello schema della tabella {TableName}", tableName);
|
|
databaseErrorMessage = $"Errore nel caricamento della tabella: {ex.Message}";
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
|
|
// 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
|
|
{
|
|
// No primary key found, require manual selection
|
|
requiresManualKeySelection = true;
|
|
sourceKeyField = "";
|
|
Logger.LogInformation("Nessuna chiave primaria trovata per la tabella {TableName}, selezione manuale richiesta", tableName);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Errore nel rilevamento della chiave primaria per la tabella {TableName}", tableName);
|
|
requiresManualKeySelection = true;
|
|
sourceKeyField = "";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// For non-database sources, always require manual selection
|
|
requiresManualKeySelection = true;
|
|
sourceKeyField = "";
|
|
}
|
|
|
|
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 availableTableNames;
|
|
|
|
return availableTableNames.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)
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gestisce la connessione al database con schema specifico
|
|
/// </summary>
|
|
private async Task ConnectToDatabaseWithSchema(string? specificSchema = null)
|
|
{
|
|
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
|
|
Logger.LogInformation("Creando database manager per credenziale: {CredentialName}", selectedDatabaseCredential);
|
|
currentDatabaseManager = await ConnectionFactory.CreateDatabaseManagerAsync(selectedDatabaseCredential);
|
|
Logger.LogInformation("Database manager creato con successo");
|
|
|
|
// Se è specificato uno schema, utilizzalo direttamente
|
|
if (!string.IsNullOrEmpty(specificSchema))
|
|
{
|
|
Logger.LogInformation("Utilizzando schema specifico: {Schema}", specificSchema);
|
|
await LoadSchemaForDatabase(specificSchema);
|
|
}
|
|
else
|
|
{
|
|
// Prova il discovery automatico dello schema
|
|
await DiscoverDatabaseSchema();
|
|
}
|
|
|
|
if (databaseTables.Count > 0)
|
|
{
|
|
isDatabaseConnected = true;
|
|
Logger.LogInformation("Connessione database completata con {TableCount} tabelle", databaseTables.Count);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Errore nella connessione al database");
|
|
databaseErrorMessage = $"Errore: {ex.Message}";
|
|
}
|
|
finally
|
|
{
|
|
isConnectingDatabase = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scopre automaticamente lo schema del database
|
|
/// </summary>
|
|
private async Task DiscoverDatabaseSchema()
|
|
{
|
|
try
|
|
{
|
|
Logger.LogInformation("Iniziando discovery automatico dello schema");
|
|
var schema = await currentDatabaseManager!.GetDatabaseSchemaAsync();
|
|
|
|
Logger.LogInformation("Schema discovery completato. Numero elementi: {Count}", schema?.Count() ?? 0);
|
|
|
|
databaseTables = schema as Dictionary<string, IEnumerable<DbColumnInfo>> ??
|
|
(schema != null ? new Dictionary<string, IEnumerable<DbColumnInfo>>(schema) : new Dictionary<string, IEnumerable<DbColumnInfo>>());
|
|
|
|
if (databaseTables.Count == 0)
|
|
{
|
|
// Se non ci sono tabelle, potrebbe essere necessario selezionare un database specifico
|
|
// Schema discovery completato senza successo
|
|
databaseErrorMessage = "Impossibile rilevare le tabelle del database. Verificare le credenziali di connessione.";
|
|
}
|
|
else
|
|
{
|
|
// Rileva e salva lo schema corrente se presente nelle chiavi delle tabelle
|
|
var firstTableKey = databaseTables.Keys.FirstOrDefault();
|
|
if (!string.IsNullOrEmpty(firstTableKey) && firstTableKey.Contains('.'))
|
|
{
|
|
var detectedSchema = firstTableKey.Split('.')[0];
|
|
Logger.LogInformation("Schema rilevato automaticamente: {Schema}", detectedSchema);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Errore durante il discovery automatico dello schema");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Carica lo schema per un database specifico
|
|
/// </summary>
|
|
private async Task LoadSchemaForDatabase(string schemaName)
|
|
{
|
|
try
|
|
{
|
|
// TODO: Implementare la logica specifica per il caricamento di uno schema
|
|
// Per ora utilizziamo il discovery standard e filtriamo i risultati
|
|
var schema = await currentDatabaseManager!.GetDatabaseSchemaAsync();
|
|
|
|
databaseTables = schema as Dictionary<string, IEnumerable<DbColumnInfo>> ??
|
|
new Dictionary<string, IEnumerable<DbColumnInfo>>();
|
|
|
|
// Filtra le tabelle per lo schema specificato
|
|
if (!string.IsNullOrEmpty(schemaName))
|
|
{
|
|
var filteredTables = databaseTables
|
|
.Where(kvp => kvp.Key.StartsWith($"{schemaName}.", StringComparison.OrdinalIgnoreCase))
|
|
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
|
|
|
if (filteredTables.Any())
|
|
{
|
|
databaseTables = filteredTables;
|
|
Logger.LogInformation("Caricate {TableCount} tabelle per lo schema {Schema}", filteredTables.Count, schemaName);
|
|
}
|
|
else
|
|
{
|
|
Logger.LogWarning("Nessuna tabella trovata per lo schema {Schema}", schemaName);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Errore nel caricamento dello schema {Schema}", schemaName);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Estrae lo schema dal nome completo di una tabella
|
|
/// </summary>
|
|
private string? ExtractSchemaFromTableName(string fullTableName)
|
|
{
|
|
if (string.IsNullOrEmpty(fullTableName) || !fullTableName.Contains('.'))
|
|
return null;
|
|
|
|
var parts = fullTableName.Split('.');
|
|
return parts.Length > 1 ? parts[0] : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ottiene lo schema correntemente utilizzato dal database connesso
|
|
/// </summary>
|
|
private string? GetCurrentDatabaseSchema()
|
|
{
|
|
if (!databaseTables.Any())
|
|
return null;
|
|
|
|
var firstTable = databaseTables.Keys.FirstOrDefault();
|
|
return !string.IsNullOrEmpty(firstTable) ? ExtractSchemaFromTableName(firstTable) : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ottiene il campo ID dell'entità REST selezionata
|
|
/// </summary>
|
|
private string GetEntityIdField()
|
|
{
|
|
if (restEntityDetails?.Properties != null)
|
|
{
|
|
// Cerca il campo ID (tipicamente "Id", "ID", "id", o il primo campo che contiene "id")
|
|
var idProperty = restEntityDetails.Properties.FirstOrDefault(p =>
|
|
p.Name.Equals("Id", StringComparison.OrdinalIgnoreCase) ||
|
|
p.Name.Equals("ID", StringComparison.OrdinalIgnoreCase) ||
|
|
p.Name.Contains("id", StringComparison.OrdinalIgnoreCase));
|
|
|
|
return idProperty?.Name ?? "Id"; // Default a "Id" se non trovato
|
|
}
|
|
return "Id";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifica se una query è una SELECT query sicura
|
|
|
|
/// </summary>
|
|
private bool IsSelectQuery(string query)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(query))
|
|
return false;
|
|
|
|
var trimmedQuery = query.Trim();
|
|
|
|
// 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>
|
|
/// Pulisce una query SQL rimuovendo caratteri pericolosi
|
|
/// </summary>
|
|
private string CleanQuery(string query)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(query))
|
|
return "";
|
|
|
|
// Rimuove caratteri potenzialmente pericolosi
|
|
var cleanQuery = query.Trim();
|
|
|
|
// Rimuove eventuali terminatori multipli
|
|
while (cleanQuery.EndsWith(";"))
|
|
{
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gestisce il cambio di modalità tra tabelle e query custom
|
|
/// </summary>
|
|
private void OnQueryModeChanged(ChangeEventArgs e)
|
|
{
|
|
useCustomQuery = (bool)(e.Value ?? false);
|
|
|
|
// Reset stato quando cambia modalità
|
|
if (useCustomQuery)
|
|
{
|
|
// Reset selezione tabella
|
|
selectedTable = "";
|
|
ClearAllMappings();
|
|
}
|
|
else
|
|
{
|
|
// Reset query custom
|
|
customQuery = "";
|
|
isQueryValid = false;
|
|
queryValidationMessage = "";
|
|
queryPreviewData.Clear();
|
|
queryColumns.Clear();
|
|
showQueryPreview = false;
|
|
}
|
|
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Valida la query SQL custom
|
|
/// </summary>
|
|
private async Task ValidateCustomQuery()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(customQuery) || currentDatabaseManager == null)
|
|
{
|
|
isQueryValid = false;
|
|
queryValidationMessage = "Query vuota o manager database non disponibile";
|
|
return;
|
|
}
|
|
|
|
isValidatingQuery = true;
|
|
|
|
try
|
|
{
|
|
// Controllo di sicurezza: verifica che sia una SELECT
|
|
if (!IsSelectQuery(customQuery))
|
|
{
|
|
isQueryValid = false;
|
|
queryValidationMessage = "Solo query SELECT sono permesse per sicurezza";
|
|
return;
|
|
}
|
|
|
|
var cleanQuery = CleanQuery(customQuery);
|
|
|
|
// Trova la credenziale per determinare il tipo di database
|
|
var credential = databaseCredentials.FirstOrDefault(c => c.Name == selectedDatabaseCredential);
|
|
if (credential == null)
|
|
{
|
|
isQueryValid = false;
|
|
queryValidationMessage = "Credenziale database non trovata";
|
|
return;
|
|
}
|
|
|
|
// Crea una query di test con sintassi appropriata per il tipo di database
|
|
var testQuery = CreateLimitedQuery(cleanQuery, credential.DatabaseType, 1);
|
|
|
|
Logger.LogInformation("Validando query: {Query}", testQuery);
|
|
|
|
// Prova a eseguire la query per validarla
|
|
var testResults = await currentDatabaseManager.ExecuteRawQueryAsync(testQuery);
|
|
|
|
if (testResults != null && testResults.Any())
|
|
{
|
|
var firstRow = testResults.First();
|
|
queryColumns = firstRow.Keys.ToList();
|
|
isQueryValid = true;
|
|
queryValidationMessage = $"Query valida - {queryColumns.Count} colonne rilevate";
|
|
|
|
// Clear mappings quando cambia la query
|
|
ClearAllMappings();
|
|
|
|
// AUTO-SELECT della chiave per query custom
|
|
TryAutoSelectKeyForQuery(queryColumns);
|
|
|
|
Logger.LogInformation("Query validata con successo: {ColumnCount} colonne", queryColumns.Count);
|
|
}
|
|
else
|
|
{
|
|
isQueryValid = false;
|
|
queryValidationMessage = "La query non ha restituito risultati o colonne";
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Errore nella validazione della query");
|
|
isQueryValid = false;
|
|
queryValidationMessage = $"Errore nella validazione: {ex.Message}";
|
|
}
|
|
finally
|
|
{
|
|
isValidatingQuery = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Carica un'anteprima dei dati della query
|
|
/// </summary>
|
|
private async Task LoadQueryPreview()
|
|
{
|
|
if (!isQueryValid || string.IsNullOrWhiteSpace(customQuery) || currentDatabaseManager == null)
|
|
return;
|
|
|
|
isLoadingPreview = true;
|
|
|
|
try
|
|
{
|
|
var cleanQuery = CleanQuery(customQuery);
|
|
|
|
// Trova la credenziale per determinare il tipo di database
|
|
var credential = databaseCredentials.FirstOrDefault(c => c.Name == selectedDatabaseCredential);
|
|
if (credential == null)
|
|
{
|
|
queryValidationMessage = "Credenziale database non trovata";
|
|
return;
|
|
}
|
|
|
|
// Crea una query di anteprima con sintassi appropriata per il tipo di database
|
|
var previewQuery = CreateLimitedQuery(cleanQuery, credential.DatabaseType, 10);
|
|
|
|
Logger.LogInformation("Caricando anteprima con query: {Query}", previewQuery);
|
|
|
|
var previewResults = await currentDatabaseManager.ExecuteRawQueryAsync(previewQuery);
|
|
queryPreviewData = previewResults.ToList();
|
|
showQueryPreview = true;
|
|
|
|
Logger.LogInformation("Caricata anteprima query con {RecordCount} record", queryPreviewData.Count);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Errore nel caricamento anteprima query");
|
|
queryValidationMessage = $"Errore anteprima: {ex.Message}";
|
|
}
|
|
finally
|
|
{
|
|
isLoadingPreview = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Crea una query limitata in base al tipo di database
|
|
/// </summary>
|
|
private string CreateLimitedQuery(string baseQuery, DatabaseType databaseType, int limit)
|
|
{
|
|
return databaseType switch
|
|
{
|
|
DatabaseType.SqlServer => $"SELECT TOP {limit} * FROM ({baseQuery}) AS subquery",
|
|
DatabaseType.Oracle => $"SELECT * FROM ({baseQuery}) WHERE ROWNUM <= {limit}",
|
|
DatabaseType.MySql => $"{baseQuery} LIMIT {limit}",
|
|
DatabaseType.PostgreSql => $"{baseQuery} LIMIT {limit}",
|
|
DatabaseType.Sqlite => $"{baseQuery} LIMIT {limit}",
|
|
DatabaseType.DB2 => $"SELECT * FROM ({baseQuery}) FETCH FIRST {limit} ROWS ONLY",
|
|
DatabaseType.SapHana => $"{baseQuery} LIMIT {limit}",
|
|
_ => $"{baseQuery} LIMIT {limit}" // Default a LIMIT per database non riconosciuti
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Nasconde l'anteprima della query
|
|
/// </summary>
|
|
private void HideQueryPreview()
|
|
{
|
|
showQueryPreview = false;
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ottiene l'ID della credenziale sorgente corrente
|
|
/// </summary>
|
|
private async Task<int?> GetCurrentSourceCredentialIdAsync()
|
|
{
|
|
if (selectedSourceType == "database" && !string.IsNullOrEmpty(selectedDatabaseCredential))
|
|
{
|
|
try
|
|
{
|
|
// Usa il nuovo metodo per ottenere direttamente l'ID della credenziale
|
|
return await CredentialService.GetCredentialIdByNameAsync(selectedDatabaseCredential, CredentialManager.Models.CredentialType.Database);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Errore nell'ottenere l'ID della credenziale database: {CredentialName}", selectedDatabaseCredential);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ottiene l'ID della credenziale destinazione corrente
|
|
/// </summary>
|
|
private async Task<int?> GetCurrentDestinationCredentialIdAsync()
|
|
{
|
|
if (!string.IsNullOrEmpty(selectedRestCredential))
|
|
{
|
|
try
|
|
{
|
|
// Usa il nuovo metodo per ottenere direttamente l'ID della credenziale
|
|
return await CredentialService.GetCredentialIdByNameAsync(selectedRestCredential, CredentialManager.Models.CredentialType.RestApi);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Errore nell'ottenere l'ID della credenziale REST: {CredentialName}", selectedRestCredential);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Annulla la selezione dello schema
|
|
/// </summary>
|
|
private void CancelSchemaSelection()
|
|
{
|
|
showSchemaSelectionModal = false;
|
|
selectedSchema = "";
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ottiene le colonne di una tabella specifica
|
|
/// </summary>
|
|
private async Task<List<DbColumnInfo>> GetTableColumns(string fullTableName, string databaseName, string tableName)
|
|
{
|
|
var columns = new List<DbColumnInfo>();
|
|
|
|
try
|
|
{
|
|
var credential = databaseCredentials.FirstOrDefault(c => c.Name == selectedDatabaseCredential);
|
|
if (credential == null) return columns;
|
|
|
|
var columnsQuery = credential.DatabaseType switch
|
|
{
|
|
DatabaseType.SqlServer => $@"
|
|
SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE,
|
|
CASE WHEN CHARACTER_MAXIMUM_LENGTH IS NOT NULL THEN CHARACTER_MAXIMUM_LENGTH
|
|
ELSE NUMERIC_PRECISION END as MAX_LENGTH
|
|
FROM {databaseName}.INFORMATION_SCHEMA.COLUMNS
|
|
WHERE TABLE_NAME = '{tableName}'
|
|
ORDER BY ORDINAL_POSITION",
|
|
|
|
DatabaseType.MySql => $@"
|
|
SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE,
|
|
CASE WHEN CHARACTER_MAXIMUM_LENGTH IS NOT NULL THEN CHARACTER_MAXIMUM_LENGTH
|
|
ELSE NUMERIC_PRECISION END as MAX_LENGTH
|
|
FROM INFORMATION_SCHEMA.COLUMNS
|
|
WHERE TABLE_SCHEMA = '{databaseName}' AND TABLE_NAME = '{tableName}'
|
|
ORDER BY ORDINAL_POSITION",
|
|
|
|
DatabaseType.PostgreSql => $@"
|
|
SELECT column_name as COLUMN_NAME, data_type as DATA_TYPE,
|
|
is_nullable as IS_NULLABLE, character_maximum_length as MAX_LENGTH
|
|
FROM information_schema.columns
|
|
WHERE table_schema = '{databaseName}' AND table_name = '{tableName}'
|
|
ORDER BY ordinal_position",
|
|
|
|
DatabaseType.Oracle => $@"
|
|
SELECT COLUMN_NAME, DATA_TYPE, NULLABLE as IS_NULLABLE, DATA_LENGTH as MAX_LENGTH
|
|
FROM ALL_TAB_COLUMNS
|
|
WHERE OWNER = '{databaseName.ToUpper()}' AND TABLE_NAME = '{tableName.ToUpper()}'
|
|
ORDER BY COLUMN_ID",
|
|
|
|
_ => $@"
|
|
SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, CHARACTER_MAXIMUM_LENGTH as MAX_LENGTH
|
|
FROM INFORMATION_SCHEMA.COLUMNS
|
|
WHERE TABLE_SCHEMA = '{databaseName}' AND TABLE_NAME = '{tableName}'
|
|
ORDER BY ORDINAL_POSITION"
|
|
};
|
|
|
|
var columnResults = await currentDatabaseManager!.ExecuteRawQueryAsync(columnsQuery);
|
|
|
|
if (columnResults != null)
|
|
{
|
|
foreach (var row in columnResults)
|
|
{
|
|
var columnName = row.GetValueOrDefault("COLUMN_NAME")?.ToString() ?? "";
|
|
var dataType = row.GetValueOrDefault("DATA_TYPE")?.ToString() ?? "";
|
|
var isNullable = row.GetValueOrDefault("IS_NULLABLE")?.ToString()?.ToUpper() == "YES";
|
|
|
|
if (int.TryParse(row.GetValueOrDefault("MAX_LENGTH")?.ToString(), out int maxLength))
|
|
{
|
|
// Usa maxLength se necessario
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(columnName))
|
|
{
|
|
columns.Add(new DbColumnInfo
|
|
{
|
|
Name = columnName,
|
|
DataType = dataType,
|
|
IsNullable = isNullable
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Errore nell'ottenere le colonne per la tabella {TableName}", fullTableName);
|
|
}
|
|
|
|
return columns;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Conferma la selezione dello schema
|
|
/// </summary>
|
|
private async Task OnSchemaSelected()
|
|
{
|
|
if (string.IsNullOrEmpty(selectedSchema))
|
|
return;
|
|
|
|
showSchemaSelectionModal = false;
|
|
|
|
try
|
|
{
|
|
Logger.LogInformation("Schema selezionato: {Schema}. Riconnessione al database...", selectedSchema);
|
|
|
|
// Riconnetti al database utilizzando lo schema selezionato
|
|
await ConnectToDatabaseWithSchema(selectedSchema);
|
|
|
|
if (isDatabaseConnected)
|
|
{
|
|
Logger.LogInformation("Connessione completata con successo usando lo schema {Schema}", selectedSchema);
|
|
databaseErrorMessage = ""; // Pulisci eventuali errori precedenti
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Errore nella connessione con lo schema selezionato");
|
|
databaseErrorMessage = $"Errore nella connessione al database {selectedSchema}: {ex.Message}";
|
|
}
|
|
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Carica la lista degli schemi disponibili
|
|
/// </summary>
|
|
private async Task LoadAvailableSchemas()
|
|
{
|
|
if (currentDatabaseManager == null)
|
|
return;
|
|
|
|
isLoadingSchemas = true;
|
|
availableSchemas.Clear();
|
|
|
|
try
|
|
{
|
|
// Prova a ottenere tutti gli schemi/database disponibili
|
|
// Questo metodo potrebbe non essere disponibile su tutti i database manager
|
|
// In tal caso, proveremo con una query diretta
|
|
try
|
|
{
|
|
var allSchemas = await currentDatabaseManager.GetDatabaseSchemaAsync();
|
|
|
|
if (allSchemas != null)
|
|
{
|
|
// Estrai i nomi degli schemi dalle chiavi delle tabelle
|
|
var schemaNames = allSchemas.Keys
|
|
.Where(key => key.Contains('.'))
|
|
.Select(key => key.Split('.')[0])
|
|
.Distinct()
|
|
.OrderBy(schema => schema)
|
|
.ToList();
|
|
|
|
if (schemaNames.Any())
|
|
{
|
|
availableSchemas.AddRange(schemaNames);
|
|
Logger.LogInformation("Rilevati {SchemaCount} schemi dalle tabelle: {Schemas}",
|
|
schemaNames.Count, string.Join(", ", schemaNames));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogWarning(ex, "Impossibile ottenere schemi dal database manager, provo con query dirette");
|
|
}
|
|
|
|
// Se il metodo sopra non funziona, prova con query SQL specifiche per database
|
|
await TryLoadSchemasWithDirectQuery();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Errore nel caricamento degli schemi disponibili");
|
|
}
|
|
finally
|
|
{
|
|
isLoadingSchemas = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Prova a caricare gli schemi con query SQL dirette
|
|
/// </summary>
|
|
private async Task TryLoadSchemasWithDirectQuery()
|
|
{
|
|
if (currentDatabaseManager == null)
|
|
return;
|
|
|
|
try
|
|
{
|
|
// Query diverse per ogni tipo di database - focalizzate sui database/cataloghi
|
|
var credential = databaseCredentials.FirstOrDefault(c => c.Name == selectedDatabaseCredential);
|
|
if (credential == null) return;
|
|
|
|
string? schemaQuery = credential.DatabaseType switch
|
|
{
|
|
DatabaseType.SqlServer => "SELECT name FROM sys.databases WHERE name NOT IN ('master', 'tempdb', 'model', 'msdb') AND state = 0",
|
|
DatabaseType.PostgreSql => "SELECT datname FROM pg_database WHERE datistemplate = false AND datname NOT IN ('postgres', 'template0', 'template1')",
|
|
DatabaseType.MySql => "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys')",
|
|
DatabaseType.Oracle => "SELECT DISTINCT OWNER FROM ALL_TABLES WHERE OWNER NOT IN ('SYS', 'SYSTEM', 'DBSNMP', 'SYSMAN', 'OUTLN', 'ANONYMOUS', 'CTXSYS', 'EXFSYS', 'LBACSYS', 'MDSYS', 'MGMT_VIEW', 'OLAPSYS', 'OWBSYS', 'ORDDATA', 'ORDSYS', 'SI_INFORMTN_SCHEMA', 'WK_TEST', 'WKPROXY', 'WMSYS', 'XDB', 'APEX_040000', 'APEX_PUBLIC_USER', 'DIP', 'FLOWS_FILES', 'HR', 'IX', 'OE', 'PM', 'SCOTT', 'SH', 'BI')",
|
|
_ => null
|
|
};
|
|
|
|
if (!string.IsNullOrEmpty(schemaQuery))
|
|
{
|
|
Logger.LogInformation("Eseguendo query per database/schemi: {Query}", schemaQuery);
|
|
var results = await currentDatabaseManager.ExecuteRawQueryAsync(schemaQuery);
|
|
|
|
if (results != null && results.Any())
|
|
{
|
|
var schemas = results.Select(row =>
|
|
{
|
|
var firstValue = row.Values.FirstOrDefault();
|
|
return firstValue?.ToString() ?? "";
|
|
})
|
|
.Where(schema => !string.IsNullOrEmpty(schema))
|
|
.OrderBy(schema => schema)
|
|
.ToList();
|
|
|
|
if (schemas.Any())
|
|
{
|
|
availableSchemas.AddRange(schemas);
|
|
Logger.LogInformation("Caricati {SchemaCount} database/schemi via query diretta per {DatabaseType}: {Schemas}",
|
|
schemas.Count, credential.DatabaseType, string.Join(", ", schemas));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogWarning(ex, "Errore nel caricamento database/schemi via query diretta");
|
|
}
|
|
}
|
|
|
|
// Gestione selezione database quando la discovery restituisce dizionario vuoto
|
|
private async Task HandleDatabaseSelectionRequired()
|
|
{
|
|
isLoadingDatabases = true;
|
|
showDatabaseSelectionModal = true;
|
|
availableDatabases.Clear();
|
|
selectedDatabase = "";
|
|
try
|
|
{
|
|
if (currentDatabaseManager != null)
|
|
{
|
|
var dbs = await currentDatabaseManager.GetAvailableDatabasesAsync();
|
|
availableDatabases = dbs ?? new List<string>();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
databaseErrorMessage = $"Errore nel caricamento dei database: {ex.Message}";
|
|
}
|
|
finally
|
|
{
|
|
isLoadingDatabases = false;
|
|
}
|
|
}
|
|
|
|
private async Task OnDatabaseSelected()
|
|
{
|
|
try
|
|
{
|
|
if (string.IsNullOrEmpty(selectedDatabase))
|
|
{
|
|
databaseErrorMessage = "Nessun database selezionato";
|
|
return;
|
|
}
|
|
|
|
showDatabaseSelectionModal = false;
|
|
|
|
Logger.LogInformation("Database selezionato: {DatabaseName}. Riconnessione in corso...", selectedDatabase);
|
|
|
|
// Riconnessione al database selezionato
|
|
await ConnectToDatabaseWithSpecificDatabase(selectedDatabase);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Errore nella selezione del database: {DatabaseName}", selectedDatabase);
|
|
databaseErrorMessage = $"Errore nella connessione al database {selectedDatabase}: {ex.Message}";
|
|
}
|
|
}
|
|
|
|
private void CancelDatabaseSelection()
|
|
{
|
|
showDatabaseSelectionModal = false;
|
|
selectedDatabase = "";
|
|
databaseErrorMessage = "Selezione database annullata";
|
|
Logger.LogInformation("Selezione database annullata dall'utente");
|
|
}
|
|
|
|
// Metodi helper per la connessione database
|
|
private Task<bool> IsDatabaseSpecifiedInConnectionString(DatabaseCredential credential)
|
|
{
|
|
try
|
|
{
|
|
Logger.LogInformation("Verifica database specificato - Tipo: {DatabaseType}, DatabaseName: '{DatabaseName}', Connection: {ConnectionString}",
|
|
credential.DatabaseType, credential.DatabaseName, credential.ConnectionString?.Substring(0, Math.Min(100, credential.ConnectionString?.Length ?? 0)));
|
|
|
|
// Prima verifica se c'è un database specificato nel campo DatabaseName della credenziale
|
|
if (!string.IsNullOrEmpty(credential.DatabaseName))
|
|
{
|
|
Logger.LogInformation("Database specificato nel campo DatabaseName: '{DatabaseName}' - RESULT: TRUE", credential.DatabaseName);
|
|
return Task.FromResult(true);
|
|
}
|
|
|
|
// Per SQL Server verifica se Initial Catalog o Database è specificato nella connection string
|
|
if (credential.DatabaseType == DatabaseType.SqlServer)
|
|
{
|
|
var connectionString = credential.ConnectionString ?? "";
|
|
var hasInitialCatalog = connectionString.Contains("Initial Catalog=", StringComparison.OrdinalIgnoreCase);
|
|
var hasDatabase = connectionString.Contains("Database=", StringComparison.OrdinalIgnoreCase);
|
|
var result = hasInitialCatalog || hasDatabase;
|
|
|
|
Logger.LogInformation("SQL Server - HasInitialCatalog: {HasInitialCatalog}, HasDatabase: {HasDatabase}, Result: {Result}",
|
|
hasInitialCatalog, hasDatabase, result);
|
|
|
|
return Task.FromResult(result);
|
|
}
|
|
|
|
// TODO: Implementare per altri tipi di database
|
|
// MySQL: Database=
|
|
// PostgreSQL: Database=
|
|
// Oracle: più complesso con SID/Service Name
|
|
|
|
Logger.LogWarning("Verifica database specificato non implementata per tipo database: {DatabaseType}", credential.DatabaseType);
|
|
return Task.FromResult(true); // Default: assume database specificato per tipi non implementati
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Errore nella verifica database specificato in connection string");
|
|
return Task.FromResult(true); // Default: assume database specificato in caso di errore
|
|
}
|
|
}
|
|
|
|
private async Task LoadTablesFromConnectedDatabase()
|
|
{
|
|
try
|
|
{
|
|
if (currentDatabaseManager == null)
|
|
{
|
|
databaseErrorMessage = "Database manager non disponibile";
|
|
return;
|
|
}
|
|
|
|
Logger.LogInformation("Caricando tabelle dal database connesso");
|
|
var tableNames = await currentDatabaseManager.GetTableNamesAsync();
|
|
availableTableNames = tableNames.ToList();
|
|
|
|
Logger.LogInformation("Caricate {Count} tabelle dal database", availableTableNames.Count);
|
|
|
|
// Resetta i dettagli delle tabelle - verranno caricati solo quando selezionati
|
|
databaseTables.Clear();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Errore nel caricamento delle tabelle dal database connesso");
|
|
databaseErrorMessage = $"Errore nel caricamento tabelle: {ex.Message}";
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private async Task LoadAvailableDatabases()
|
|
{
|
|
try
|
|
{
|
|
if (currentDatabaseManager == null)
|
|
{
|
|
databaseErrorMessage = "Database manager non disponibile";
|
|
return;
|
|
}
|
|
|
|
isLoadingDatabases = true;
|
|
Logger.LogInformation("Caricando database disponibili");
|
|
|
|
// Usa il metodo corretto dell'interfaccia IDatabaseManager
|
|
var allDatabases = await currentDatabaseManager.GetAvailableDatabasesAsync();
|
|
Logger.LogInformation("Ottenuti {DatabaseCount} database dal server", allDatabases.Count);
|
|
|
|
// Filtra i database di sistema
|
|
availableDatabases = FilterSystemDatabases(allDatabases).ToList();
|
|
|
|
Logger.LogInformation("Trovati {TotalDatabases} database, filtrati a {FilteredDatabases} (esclusi quelli di sistema)",
|
|
allDatabases.Count, availableDatabases.Count);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Errore nel caricamento dei database disponibili");
|
|
databaseErrorMessage = $"Errore nel caricamento database: {ex.Message}";
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
isLoadingDatabases = false;
|
|
}
|
|
}
|
|
|
|
private IEnumerable<string> FilterSystemDatabases(List<string> allDatabases)
|
|
{
|
|
// Trova la credenziale per determinare il tipo di database
|
|
var credential = databaseCredentials.FirstOrDefault(c => c.Name == selectedDatabaseCredential);
|
|
if (credential == null)
|
|
{
|
|
Logger.LogWarning("Credenziale non trovata per filtraggio database di sistema");
|
|
return allDatabases; // Restituisce tutti se non riesce a determinare il tipo
|
|
}
|
|
|
|
var databaseType = credential.DatabaseType;
|
|
|
|
// Filtri per SQL Server
|
|
if (databaseType == DatabaseType.SqlServer)
|
|
{
|
|
var sqlServerSystemDatabases = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
"master", "tempdb", "model", "msdb", "Resource", "mssqlsystemresource",
|
|
"ReportServer", "ReportServerTempDB", "SSISDB", "distribution"
|
|
};
|
|
|
|
return allDatabases.Where(db => !sqlServerSystemDatabases.Contains(db));
|
|
}
|
|
|
|
// TODO: Implementare filtri per altri tipi di database
|
|
if (databaseType == DatabaseType.MySql)
|
|
{
|
|
Logger.LogInformation("Filtro database di sistema MySQL - DA IMPLEMENTARE");
|
|
return allDatabases; // Per ora restituisce tutti
|
|
}
|
|
|
|
if (databaseType == DatabaseType.PostgreSql)
|
|
{
|
|
Logger.LogInformation("Filtro database di sistema PostgreSQL - DA IMPLEMENTARE");
|
|
return allDatabases; // Per ora restituisce tutti
|
|
}
|
|
|
|
if (databaseType == DatabaseType.Oracle)
|
|
{
|
|
Logger.LogInformation("Filtro database di sistema Oracle - DA IMPLEMENTARE");
|
|
return allDatabases; // Per ora restituisce tutti
|
|
}
|
|
|
|
Logger.LogWarning("Tipo database non riconosciuto per filtraggio: {DatabaseType}", databaseType);
|
|
return allDatabases; // Restituisce tutti per tipi non riconosciuti
|
|
}
|
|
|
|
// Metodo per gestire la selezione di un database dal modale
|
|
|
|
|
|
private async Task ConnectToDatabaseWithSpecificDatabase(string databaseName)
|
|
{
|
|
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
|
|
Logger.LogInformation("Creando database manager per credenziale: {CredentialName}", selectedDatabaseCredential);
|
|
currentDatabaseManager = await ConnectionFactory.CreateDatabaseManagerAsync(selectedDatabaseCredential);
|
|
Logger.LogInformation("Database manager creato con successo");
|
|
if (currentDatabaseManager == null)
|
|
{
|
|
databaseErrorMessage = "Database manager non disponibile";
|
|
return;
|
|
}
|
|
|
|
Logger.LogInformation("Cambiando database a: {DatabaseName}", databaseName);
|
|
|
|
// Usa il metodo dell'interfaccia per cambiare database
|
|
await currentDatabaseManager.ChangeDatabaseAsync(databaseName);
|
|
Logger.LogInformation("Database cambiato con successo a: {DatabaseName}", databaseName);
|
|
|
|
// Carica le tabelle dal database selezionato
|
|
await LoadTablesFromConnectedDatabase();
|
|
|
|
isDatabaseConnected = true;
|
|
Logger.LogInformation("Connessione completata per database: {DatabaseName}", databaseName);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Errore nella connessione al database specifico: {DatabaseName}", databaseName);
|
|
databaseErrorMessage = $"Errore nella connessione al database {databaseName}: {ex.Message}";
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <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", "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));
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
private async Task<string> GenerateUniqueProfileName(string baseName)
|
|
{
|
|
var uniqueName = baseName;
|
|
var counter = 1;
|
|
|
|
while (await ProfileService.GetProfileByNameIncludingInactiveAsync(uniqueName) != null)
|
|
{
|
|
uniqueName = $"{baseName} ({counter})";
|
|
counter++;
|
|
}
|
|
|
|
return uniqueName;
|
|
}
|
|
}
|
|
|