diff --git a/Components/ProfileQuickActions.razor b/Components/ProfileQuickActions.razor deleted file mode 100644 index e69de29..0000000 diff --git a/Components/ProfileQuickActions.razor.cs b/Components/ProfileQuickActions.razor.cs deleted file mode 100644 index e69de29..0000000 diff --git a/Components/ProfileSaver.razor b/Components/ProfileSaver.razor index 2699635..9eeb893 100644 --- a/Components/ProfileSaver.razor +++ b/Components/ProfileSaver.razor @@ -44,6 +44,22 @@ + +
+ +
+ @* Contenuto esistente per la fonte *@ +
+
+ + +
+ +
+ @* Contenuto esistente per la destinazione *@ +
+
+ @if (!string.IsNullOrEmpty(SaveMessage)) {
@@ -63,6 +79,10 @@ { Credenziali: @SourceCredentialName
} + @if (!string.IsNullOrEmpty(SourceDatabaseName)) + { + Database: @SourceDatabaseName (dalla connessione attiva)
+ } @if (!string.IsNullOrEmpty(SourceSchema)) { Schema: @SourceSchema
diff --git a/Components/ProfileSaver.razor.cs b/Components/ProfileSaver.razor.cs index 2077d4a..2fd5a96 100644 --- a/Components/ProfileSaver.razor.cs +++ b/Components/ProfileSaver.razor.cs @@ -1,15 +1,19 @@ using Microsoft.AspNetCore.Components; using CredentialManager.Models; +using CredentialManager.Services; using System.ComponentModel.DataAnnotations; namespace Components; public partial class ProfileSaver { + [Inject] private ICredentialService CredentialService { get; set; } = default!; + [Parameter] public bool CanSave { get; set; } [Parameter] public string SourceType { get; set; } = ""; [Parameter] public int? SourceCredentialId { get; set; } [Parameter] public string? SourceCredentialName { get; set; } + [Parameter] public string? SourceDatabaseName { get; set; } [Parameter] public string? SourceSchema { get; set; } [Parameter] public string? SourceTable { get; set; } [Parameter] public string? SourceFilePath { get; set; } @@ -51,6 +55,9 @@ public partial class ProfileSaver try { + // Recupera automaticamente il nome del database dalla connessione attiva + var sourceDatabaseName = await GetSourceDatabaseNameAsync(); + var profileDto = new DataCouplerProfileDto { Name = ProfileData.Name, @@ -58,6 +65,7 @@ public partial class ProfileSaver SourceType = SourceType, SourceCredentialId = SourceCredentialId, SourceCredentialName = SourceCredentialName, + SourceDatabaseName = sourceDatabaseName, SourceSchema = SourceSchema, SourceTable = SourceTable, SourceFilePath = SourceFilePath, @@ -119,6 +127,34 @@ public partial class ProfileSaver SaveMessageType = type; } + private async Task GetSourceDatabaseNameAsync() + { + // Prima priorità: se SourceDatabaseName è già impostato come parametro, usa quello + if (!string.IsNullOrEmpty(SourceDatabaseName)) + { + return SourceDatabaseName; + } + + // Seconda priorità: se abbiamo un SourceCredentialId, recupera il database dalle credenziali + if (SourceCredentialId.HasValue) + { + try + { + var credential = await CredentialService.GetDatabaseCredentialAsync(SourceCredentialId.Value); + if (credential != null && !string.IsNullOrEmpty(credential.DatabaseName)) + { + return credential.DatabaseName; + } + } + catch (Exception) + { + // Se non riesce a recuperare le credenziali, continua con null + } + } + + return null; + } + public class ProfileFormModel { [Required(ErrorMessage = "Il nome del profilo è obbligatorio")] diff --git a/CredentialManager/Migrations/20250704135720_AddSourceDatabaseNameColumn.Designer.cs b/CredentialManager/Migrations/20250704135720_AddSourceDatabaseNameColumn.Designer.cs new file mode 100644 index 0000000..e21eb3f --- /dev/null +++ b/CredentialManager/Migrations/20250704135720_AddSourceDatabaseNameColumn.Designer.cs @@ -0,0 +1,337 @@ +// +using System; +using CredentialManager.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CredentialManager.Migrations +{ + [DbContext(typeof(CredentialDbContext))] + [Migration("20250704135720_AddSourceDatabaseNameColumn")] + partial class AddSourceDatabaseNameColumn + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + + modelBuilder.Entity("CredentialManager.Models.CredentialEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdditionalParameters") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("CommandTimeout") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(30); + + b.Property("ConnectionString") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DatabaseName") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DatabaseType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("EncryptedApiKey") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("EncryptedAuthToken") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("EncryptedPassword") + .HasColumnType("TEXT"); + + b.Property("Headers") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("Host") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("IgnoreSslErrors") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.Property("RestServiceType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TimeoutSeconds") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(100); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Username") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DatabaseType"); + + b.HasIndex("IsActive"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("Type"); + + b.ToTable("Credentials", (string)null); + }); + + modelBuilder.Entity("CredentialManager.Models.DataCouplerProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("CreatedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DestinationCredentialId") + .HasColumnType("INTEGER"); + + b.Property("DestinationEndpoint") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DestinationSchema") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DestinationTable") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DestinationType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("FieldMappingJson") + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("LastUsedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SourceCredentialId") + .HasColumnType("INTEGER"); + + b.Property("SourceDatabaseName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SourceFilePath") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("SourceKeyField") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SourceSchema") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SourceTable") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SourceType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("UseRecordAssociations") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("DestinationCredentialId"); + + b.HasIndex("DestinationType"); + + b.HasIndex("IsActive"); + + b.HasIndex("LastUsedAt"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("SourceCredentialId"); + + b.HasIndex("SourceType"); + + b.ToTable("DataCouplerProfiles", (string)null); + }); + + modelBuilder.Entity("CredentialManager.Models.KeyAssociation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdditionalInfo") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DestinationEntity") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DestinationId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DestinationKeyField") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("KeyValue") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("LastVerifiedAt") + .HasColumnType("TEXT"); + + b.Property("RestCredentialName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SourceKeyField") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SourcesInfo") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("DestinationEntity"); + + b.HasIndex("IsActive"); + + b.HasIndex("KeyValue") + .HasDatabaseName("IX_KeyAssociations_KeyValue"); + + b.HasIndex("LastVerifiedAt"); + + b.HasIndex("RestCredentialName"); + + b.HasIndex("KeyValue", "DestinationEntity", "RestCredentialName") + .IsUnique() + .HasDatabaseName("IX_KeyAssociations_Unique"); + + b.ToTable("KeyAssociations", (string)null); + }); + + modelBuilder.Entity("CredentialManager.Models.DataCouplerProfile", b => + { + b.HasOne("CredentialManager.Models.CredentialEntity", "DestinationCredential") + .WithMany() + .HasForeignKey("DestinationCredentialId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("CredentialManager.Models.CredentialEntity", "SourceCredential") + .WithMany() + .HasForeignKey("SourceCredentialId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("DestinationCredential"); + + b.Navigation("SourceCredential"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CredentialManager/Migrations/20250704135720_AddSourceDatabaseNameColumn.cs b/CredentialManager/Migrations/20250704135720_AddSourceDatabaseNameColumn.cs new file mode 100644 index 0000000..3e0b17d --- /dev/null +++ b/CredentialManager/Migrations/20250704135720_AddSourceDatabaseNameColumn.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CredentialManager.Migrations +{ + /// + public partial class AddSourceDatabaseNameColumn : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SourceDatabaseName", + table: "DataCouplerProfiles", + type: "TEXT", + maxLength: 200, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "SourceDatabaseName", + table: "DataCouplerProfiles"); + } + } +} diff --git a/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs b/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs index 80c149e..60ff537 100644 --- a/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs +++ b/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs @@ -182,6 +182,10 @@ namespace CredentialManager.Migrations b.Property("SourceCredentialId") .HasColumnType("INTEGER"); + b.Property("SourceDatabaseName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + b.Property("SourceFilePath") .HasMaxLength(500) .HasColumnType("TEXT"); diff --git a/CredentialManager/Models/DataCouplerProfile.cs b/CredentialManager/Models/DataCouplerProfile.cs index a188225..9c4ab8d 100644 --- a/CredentialManager/Models/DataCouplerProfile.cs +++ b/CredentialManager/Models/DataCouplerProfile.cs @@ -25,6 +25,9 @@ public class DataCouplerProfile public int? SourceCredentialId { get; set; } + [MaxLength(200)] + public string? SourceDatabaseName { get; set; } + [MaxLength(200)] public string? SourceSchema { get; set; } diff --git a/CredentialManager/Models/DataCouplerProfileDto.cs b/CredentialManager/Models/DataCouplerProfileDto.cs index e458c57..a60a043 100644 --- a/CredentialManager/Models/DataCouplerProfileDto.cs +++ b/CredentialManager/Models/DataCouplerProfileDto.cs @@ -13,6 +13,7 @@ public class DataCouplerProfileDto public string SourceType { get; set; } = string.Empty; public int? SourceCredentialId { get; set; } public string? SourceCredentialName { get; set; } + public string? SourceDatabaseName { get; set; } public string? SourceSchema { get; set; } public string? SourceTable { get; set; } public string? SourceFilePath { get; set; } diff --git a/CredentialManager/Services/DataCouplerProfileService.cs b/CredentialManager/Services/DataCouplerProfileService.cs index 98c5053..7c23422 100644 --- a/CredentialManager/Services/DataCouplerProfileService.cs +++ b/CredentialManager/Services/DataCouplerProfileService.cs @@ -214,6 +214,7 @@ public class DataCouplerProfileService : IDataCouplerProfileService SourceType = profile.SourceType, SourceCredentialId = profile.SourceCredentialId, SourceCredentialName = profile.SourceCredential?.Name, + SourceDatabaseName = profile.SourceDatabaseName, SourceSchema = profile.SourceSchema, SourceTable = profile.SourceTable, SourceFilePath = profile.SourceFilePath, @@ -241,6 +242,7 @@ public class DataCouplerProfileService : IDataCouplerProfileService Description = dto.Description, SourceType = dto.SourceType, SourceCredentialId = dto.SourceCredentialId, + SourceDatabaseName = dto.SourceDatabaseName, SourceSchema = dto.SourceSchema, SourceTable = dto.SourceTable, SourceFilePath = dto.SourceFilePath, diff --git a/CredentialManager/design_time_temp.db b/CredentialManager/design_time_temp.db index 88eb0a9..cf8a0be 100644 Binary files a/CredentialManager/design_time_temp.db and b/CredentialManager/design_time_temp.db differ diff --git a/Data_Coupler/Pages/DataCoupler.razor b/Data_Coupler/Pages/DataCoupler.razor index 1712ee9..4f0cb22 100644 --- a/Data_Coupler/Pages/DataCoupler.razor +++ b/Data_Coupler/Pages/DataCoupler.razor @@ -1005,6 +1005,7 @@ SourceType="@selectedSourceType" SourceCredentialId="@(GetCurrentSourceCredentialIdAsync().Result)" SourceCredentialName="@selectedDatabaseCredential" + SourceDatabaseName="@selectedDatabase" SourceSchema="@GetCurrentDatabaseSchema()" SourceTable="@(useCustomQuery ? "custom_query" : selectedTable)" SourceFilePath="@selectedFileName" diff --git a/Data_Coupler/Pages/DataCoupler.razor.cs b/Data_Coupler/Pages/DataCoupler.razor.cs index 59c64e3..6806bab 100644 --- a/Data_Coupler/Pages/DataCoupler.razor.cs +++ b/Data_Coupler/Pages/DataCoupler.razor.cs @@ -38,42 +38,42 @@ public partial class DataCoupler : ComponentBase // Stato delle credenziali private List databaseCredentials = new(); private List 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 availableTableNames = new(); // Solo nomi delle tabelle private Dictionary> 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 availableDatabases = new(); private string selectedDatabase = ""; private bool showDatabaseSelectionModal = false; private bool isLoadingDatabases = false; - + // Database selection (schemas only) private List availableSchemas = new(); private string selectedSchema = ""; private bool showSchemaSelectionModal = false; private bool isLoadingSchemas = false; - + // Custom query functionality private bool useCustomQuery = false; private string customQuery = ""; @@ -90,42 +90,42 @@ public partial class DataCoupler : ComponentBase private Dictionary> fileSheets = new(); // SheetName -> Columns private Dictionary>> 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) ? + private int GetTotalPages(string sheetName) => fileData.ContainsKey(sheetName) ? (int)Math.Ceiling((double)fileData[sheetName].Count / pageSize) : 0; - + // REST discovery private List restEntities = new(); private RestEntitySummary? selectedRestEntity = null; private RestEntityInfo? restEntityDetails = null; private string restSearchTerm = ""; - // Mapping campi + // Mapping campi private Dictionary fieldMappings = new(); // DbColumn -> RestProperty private HashSet 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 transferResults = new(); private bool showDetailedResults = false; - + // Servizi private IDatabaseManager? currentDatabaseManager = null; private IRestMetadataDiscovery? currentRestDiscovery = null; private IRestServiceClient? currentRestClient = null; - + // Gestione Profili private List availableProfiles = new(); private bool isLoadingProfiles = false; @@ -134,7 +134,8 @@ public partial class DataCoupler : ComponentBase protected override async Task OnInitializedAsync() { await LoadCredentials(); - } private async Task LoadCredentials() + } + private async Task LoadCredentials() { try { @@ -178,7 +179,7 @@ public partial class DataCoupler : ComponentBase // Applica la configurazione del profilo await ApplyProfileConfiguration(profile); - + // Ricarica i profili per aggiornare la data di ultimo utilizzo await LoadProfiles(); } @@ -193,34 +194,34 @@ public partial class DataCoupler : ComponentBase { Logger.LogInformation("=== INIZIO APPLICAZIONE PROFILO ==="); Logger.LogInformation("Applicando configurazione profilo: {ProfileName}", profile.Name); - Logger.LogInformation("Profilo - SourceType: {SourceType}, SourceCredentialId: {SourceCredentialId}, DestinationCredentialId: {DestinationCredentialId}", + 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}", + 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}", + 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); @@ -228,38 +229,45 @@ public partial class DataCoupler : ComponentBase { 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..."); - if (!string.IsNullOrEmpty(profile.SourceSchema)) + + // 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 schema specifico"); + Logger.LogInformation("Connessione senza database/schema specifico"); await ConnectToDatabase(); } - - Logger.LogInformation("Stato dopo connessione database - Connected: {Connected}, Tables: {TableCount}", + + Logger.LogInformation("Stato dopo connessione database - Connected: {Connected}, Tables: {TableCount}", isDatabaseConnected, availableTableNames.Count); - + // Seleziona la tabella se specificata e se la connessione è riuscita 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}", + Logger.LogInformation("Tabella selezionata: {SelectedTable}, Schema caricato: {SchemaLoaded}", selectedTable, databaseTables.ContainsKey(profile.SourceTable)); } else { - Logger.LogWarning("Impossibile selezionare tabella - Table: {Table}, Connected: {Connected}", + Logger.LogWarning("Impossibile selezionare tabella - Table: {Table}, Connected: {Connected}", profile.SourceTable, isDatabaseConnected); } } @@ -282,32 +290,32 @@ public partial class DataCoupler : ComponentBase { 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}", + + 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) { @@ -316,18 +324,18 @@ public partial class DataCoupler : ComponentBase { Logger.LogInformation("Selezione entità REST: {Entity}", entity.Name); await SelectRestEntity(entity); - Logger.LogInformation("Entità REST selezionata: {SelectedEntity}, Dettagli caricati: {DetailsLoaded}", + Logger.LogInformation("Entità REST selezionata: {SelectedEntity}, Dettagli caricati: {DetailsLoaded}", selectedRestEntity?.Name, restEntityDetails != null); } else { - Logger.LogWarning("Entità REST non trovata: {Endpoint} - Entities disponibili: {Entities}", + 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}", + Logger.LogWarning("Impossibile selezionare entità REST - Endpoint: {Endpoint}, Connected: {Connected}", profile.DestinationEndpoint, isRestConnected); } } @@ -352,13 +360,13 @@ public partial class DataCoupler : ComponentBase { 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; @@ -366,11 +374,11 @@ public partial class DataCoupler : ComponentBase { keyFields.Add(mapping.DestinationField); } - Logger.LogInformation("Mapping applicato: {Source} -> {Destination} (IsKey: {IsKey})", + Logger.LogInformation("Mapping applicato: {Source} -> {Destination} (IsKey: {IsKey})", mapping.SourceField, mapping.DestinationField, mapping.IsKey); } - - Logger.LogInformation("Mappings applicati - Totale: {MappingCount}, Chiavi: {KeyCount}", + + Logger.LogInformation("Mappings applicati - Totale: {MappingCount}, Chiavi: {KeyCount}", fieldMappings.Count, keyFields.Count); } catch (Exception ex) @@ -399,7 +407,7 @@ public partial class DataCoupler : ComponentBase 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}", + Logger.LogInformation("Stato finale - Source: {SourceType}, DatabaseConnected: {DatabaseConnected}, RestConnected: {RestConnected}, Mappings: {MappingCount}", selectedSourceType, isDatabaseConnected, isRestConnected, fieldMappings.Count); } catch (Exception ex) @@ -420,26 +428,26 @@ public partial class DataCoupler : ComponentBase 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}", + 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; @@ -456,22 +464,22 @@ public partial class DataCoupler : ComponentBase 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("confirm", + var confirmOverwrite = await JSRuntime.InvokeAsync("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; @@ -487,40 +495,40 @@ public partial class DataCoupler : ComponentBase 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("confirm", + var useUniqueName = await JSRuntime.InvokeAsync("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", + await JSRuntime.InvokeVoidAsync("alert", $"Errore: Non è stato possibile generare un nome unico per il profilo. " + "Prova a ricaricare la pagina e riprova."); } @@ -537,29 +545,29 @@ public partial class DataCoupler : ComponentBase 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("confirm", + var handleDuplicate = await JSRuntime.InvokeAsync("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 @@ -569,7 +577,7 @@ public partial class DataCoupler : ComponentBase profile.Id = duplicateProfile.Id; await ProfileService.UpdateProfileAsync(profile); await LoadProfiles(); - + await JSRuntime.InvokeVoidAsync("alert", $"Profilo '{profileDto.Name}' aggiornato con successo!"); } else @@ -582,10 +590,10 @@ public partial class DataCoupler : ComponentBase // 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}'!"); } } @@ -599,11 +607,11 @@ public partial class DataCoupler : ComponentBase 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", + 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."); @@ -644,7 +652,7 @@ public partial class DataCoupler : ComponentBase private bool CanSaveProfile() { - return !string.IsNullOrEmpty(selectedSourceType) && + return !string.IsNullOrEmpty(selectedSourceType) && (!string.IsNullOrEmpty(selectedDatabaseCredential) || !string.IsNullOrEmpty(selectedRestCredential)) && (!string.IsNullOrEmpty(selectedRestCredential) || !string.IsNullOrEmpty(selectedTable)); } @@ -652,7 +660,7 @@ public partial class DataCoupler : ComponentBase private List GetCurrentFieldMappings() { var mappings = new List(); - + foreach (var mapping in fieldMappings) { mappings.Add(new FieldMappingDto @@ -664,7 +672,7 @@ public partial class DataCoupler : ComponentBase DataType = "", // TODO: Determina dai metadati }); } - + return mappings; } @@ -691,17 +699,19 @@ public partial class DataCoupler : ComponentBase currentRestDiscovery = null; currentRestClient = null; } - + private void OnSourceTypeChanged(ChangeEventArgs e) { selectedSourceType = e.Value?.ToString() ?? ""; - + // Reset state when changing source type - ResetSourceState(); } private void ResetSourceState() + ResetSourceState(); + } + private void ResetSourceState() { // Reset database state ResetDatabaseState(); - + // Reset file state selectedFileName = ""; isProcessingFile = false; @@ -709,26 +719,27 @@ public partial class DataCoupler : ComponentBase fileSheets.Clear(); fileData.Clear(); selectedSheet = ""; - + // Reset pagination currentPage = 1; - + // Reset mappings ClearAllMappings(); } private async Task OnFileSelected(InputFileChangeEventArgs e) - { try + { + 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") @@ -736,7 +747,7 @@ public partial class DataCoupler : ComponentBase fileErrorMessage = "Formato file non supportato. Utilizzare Excel (.xlsx, .xls) o CSV (.csv)"; return; } - + // Process file based on type if (extension == ".csv") { @@ -757,75 +768,77 @@ public partial class DataCoupler : ComponentBase isProcessingFile = false; StateHasChanged(); } - } private async Task ProcessCsvFile(IBrowserFile file) + } + 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)); + + 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>(); 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(); - for (int i = 0; i < headers.Count; i++) + 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; - + 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}", + + 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 ParseCsvLine(string line, char separator = ',') + } + private List ParseCsvLine(string line, char separator = ',') { var result = new List(); 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] == '"') @@ -851,21 +864,22 @@ public partial class DataCoupler : ComponentBase current.Append(c); } } - + // Add the last field result.Add(current.ToString().Trim()); - + return result; - }private async Task ProcessExcelFile(IBrowserFile file) + } + 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); @@ -893,8 +907,8 @@ public partial class DataCoupler : ComponentBase // Converti in DataSet var dataSet = reader.AsDataSet(configuration); - - Logger.LogInformation("Excel file processed: {FileName}, Sheets: {SheetCount}", + + Logger.LogInformation("Excel file processed: {FileName}, Sheets: {SheetCount}", file.Name, dataSet.Tables.Count); // Processa ogni foglio @@ -910,7 +924,7 @@ public partial class DataCoupler : ComponentBase headers.Add(column.ColumnName); } - Logger.LogInformation("Processing Excel sheet: {SheetName}, Columns: {ColumnCount}, Rows: {RowCount}", + Logger.LogInformation("Processing Excel sheet: {SheetName}, Columns: {ColumnCount}, Rows: {RowCount}", sheetName, headers.Count, table.Rows.Count); // Processa le righe di dati @@ -930,7 +944,7 @@ public partial class DataCoupler : ComponentBase // Log delle prime 3 righe per debug if (i < 3) { - Logger.LogInformation("Excel row {RowNumber} in {Sheet}: {Values}", + Logger.LogInformation("Excel row {RowNumber} in {Sheet}: {Values}", i + 1, sheetName, string.Join(" | ", rowData.Values)); } } @@ -939,7 +953,7 @@ public partial class DataCoupler : ComponentBase fileSheets[sheetName] = headers; fileData[sheetName] = dataRows; - Logger.LogInformation("Excel sheet completed: {SheetName}, Headers: {Headers}, Rows: {RowCount}", + Logger.LogInformation("Excel sheet completed: {SheetName}, Headers: {Headers}, Rows: {RowCount}", sheetName, string.Join(", ", headers), dataRows.Count); } @@ -948,7 +962,8 @@ public partial class DataCoupler : ComponentBase { selectedSheet = fileSheets.First().Key; Logger.LogInformation("Auto-selected first sheet: {SheetName}", selectedSheet); - } Logger.LogInformation("Excel file processing completed: {FileName}, Total sheets: {SheetCount}, Selected: {SelectedSheet}", + } + Logger.LogInformation("Excel file processing completed: {FileName}, Total sheets: {SheetCount}, Selected: {SelectedSheet}", file.Name, fileSheets.Count, selectedSheet); } } @@ -957,30 +972,31 @@ public partial class DataCoupler : ComponentBase 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) + } + 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(); } @@ -1018,7 +1034,8 @@ public partial class DataCoupler : ComponentBase if (string.IsNullOrEmpty(selectedSheet) || !fileData.ContainsKey(selectedSheet)) return 0; return (currentPage - 1) * pageSize + 1; - } private int GetEndRecord() + } + private int GetEndRecord() { if (string.IsNullOrEmpty(selectedSheet) || !fileData.ContainsKey(selectedSheet)) return 0; @@ -1035,37 +1052,40 @@ public partial class DataCoupler : ComponentBase currentPage = 1; // Reset to first page when changing page size StateHasChanged(); } - }private void OnDatabaseCredentialChanged(ChangeEventArgs e) + } + private void OnDatabaseCredentialChanged(ChangeEventArgs e) { selectedDatabaseCredential = e.Value?.ToString() ?? ""; ResetDatabaseState(); - } private void OnRestCredentialChanged(ChangeEventArgs e) + } + 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() + } + 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 = ""; @@ -1076,13 +1096,14 @@ public partial class DataCoupler : ComponentBase queryColumns.Clear(); showQueryPreview = false; isLoadingPreview = false; - + currentDatabaseManager?.Dispose(); currentDatabaseManager = null; - + // Clear mappings when resetting database state ClearAllMappings(); - } private void ResetRestState() + } + private void ResetRestState() { isRestConnected = false; restEntities.Clear(); @@ -1092,17 +1113,18 @@ public partial class DataCoupler : ComponentBase restErrorMessage = ""; currentRestDiscovery = null; currentRestClient = null; - + // Clear mappings when resetting REST state ClearAllMappings(); - } private async Task ConnectToDatabase() + } + private async Task ConnectToDatabase() { if (string.IsNullOrEmpty(selectedDatabaseCredential)) return; isConnectingDatabase = true; databaseErrorMessage = ""; - + try { // Trova la credenziale @@ -1125,10 +1147,10 @@ public partial class DataCoupler : ComponentBase 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."); @@ -1150,7 +1172,7 @@ public partial class DataCoupler : ComponentBase { Logger.LogInformation("Database non specificato nella connection string. Caricando database disponibili."); await LoadAvailableDatabases(); - + if (availableDatabases.Any()) { Logger.LogInformation("Trovati {DatabaseCount} database disponibili", availableDatabases.Count); @@ -1175,14 +1197,15 @@ public partial class DataCoupler : ComponentBase isConnectingDatabase = false; StateHasChanged(); } - }private async Task ConnectToRestApi() + } + private async Task ConnectToRestApi() { if (string.IsNullOrEmpty(selectedRestCredential)) return; isConnectingRest = true; restErrorMessage = ""; - + try { // Trova la credenziale @@ -1201,7 +1224,7 @@ public partial class DataCoupler : ComponentBase 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); + 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(); @@ -1216,9 +1239,9 @@ public partial class DataCoupler : ComponentBase // 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"); @@ -1237,10 +1260,11 @@ public partial class DataCoupler : ComponentBase { isConnectingRest = false; } - } private async Task SelectTable(string tableName) + } + private async Task SelectTable(string tableName) { selectedTable = tableName; - + // Clear custom query state when selecting a table useCustomQuery = false; customQuery = ""; @@ -1249,15 +1273,15 @@ public partial class DataCoupler : ComponentBase 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) { @@ -1272,7 +1296,7 @@ public partial class DataCoupler : ComponentBase 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) { @@ -1282,11 +1306,11 @@ public partial class DataCoupler : ComponentBase 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 @@ -1310,23 +1334,24 @@ public partial class DataCoupler : ComponentBase 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); } + restEntityDetails = await currentRestDiscovery.DiscoverEntityDetailsAsync(entity.Name); + } else { restErrorMessage = "Servizio di discovery REST non disponibile"; @@ -1337,15 +1362,16 @@ public partial class DataCoupler : ComponentBase { 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 GetFilteredDatabaseTables() { if (string.IsNullOrEmpty(databaseSearchTerm)) return availableTableNames; - - return availableTableNames.Where(table => + + return availableTableNames.Where(table => table.Contains(databaseSearchTerm, StringComparison.OrdinalIgnoreCase)); } @@ -1353,8 +1379,8 @@ public partial class DataCoupler : ComponentBase { if (string.IsNullOrEmpty(restSearchTerm)) return restEntities; - - return restEntities.Where(entity => + + return restEntities.Where(entity => entity.Name.Contains(restSearchTerm, StringComparison.OrdinalIgnoreCase) || (!string.IsNullOrEmpty(entity.Label) && entity.Label.Contains(restSearchTerm, StringComparison.OrdinalIgnoreCase))); } @@ -1407,9 +1433,9 @@ public partial class DataCoupler : ComponentBase // Crea il nuovo mapping fieldMappings[selectedDbColumn] = selectedRestProperty; - + Logger.LogInformation("Creato mapping: {DbColumn} -> {RestProperty}", selectedDbColumn, selectedRestProperty); - + // Deseleziona i campi selectedDbColumn = ""; selectedRestProperty = ""; @@ -1422,7 +1448,8 @@ public partial class DataCoupler : ComponentBase fieldMappings.Remove(selectedDbColumn); Logger.LogInformation("Rimosso mapping per campo: {DbColumn}", selectedDbColumn); - } private void RemoveSpecificMapping(string dbColumn) + } + private void RemoveSpecificMapping(string dbColumn) { if (fieldMappings.ContainsKey(dbColumn)) { @@ -1448,7 +1475,7 @@ public partial class DataCoupler : ComponentBase return; IEnumerable sourceColumns = new List(); - + // Ottiene le colonne in base al tipo di sorgente if (selectedSourceType == "database") { @@ -1475,7 +1502,7 @@ public partial class DataCoupler : ComponentBase foreach (var sourceColumn in sourceColumns) { // Trova una proprietà REST con nome simile - var matchingProperty = restProperties.FirstOrDefault(p => + 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) @@ -1486,11 +1513,12 @@ public partial class DataCoupler : ComponentBase fieldMappings[sourceColumn] = matchingProperty.Name; mappingsCreated++; } - } + } - Logger.LogInformation("Auto-mapping completato. Creati {Count} mapping automatici da {SourceType}", + Logger.LogInformation("Auto-mapping completato. Creati {Count} mapping automatici da {SourceType}", mappingsCreated, useCustomQuery ? "query custom" : selectedSourceType); - } private async Task ShowMappingSummary() + } + private async Task ShowMappingSummary() { var summary = "Riepilogo Configurazione:\n\n"; summary += "=== MAPPING CAMPI ===\n"; @@ -1498,16 +1526,17 @@ public partial class DataCoupler : ComponentBase { 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() + } + private async Task StartDataTransfer() { if (!fieldMappings.Any() || currentRestClient == null || selectedRestEntity == null) { @@ -1525,7 +1554,7 @@ public partial class DataCoupler : ComponentBase transferMessageType = "error"; return; } - + if (useCustomQuery) { if (!isQueryValid || string.IsNullOrWhiteSpace(customQuery)) @@ -1542,14 +1571,14 @@ public partial class DataCoupler : ComponentBase 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)) { @@ -1565,10 +1594,10 @@ public partial class DataCoupler : ComponentBase try { - var sourceName = selectedSourceType == "database" + var sourceName = selectedSourceType == "database" ? (useCustomQuery ? "custom_query" : selectedTable) : selectedSheet; - Logger.LogInformation("Iniziando trasferimento dati da {SourceType} {Source} a {Entity} con {MappingCount} mappature", + 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 @@ -1590,7 +1619,7 @@ public partial class DataCoupler : ComponentBase .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)); } @@ -1610,38 +1639,38 @@ public partial class DataCoupler : ComponentBase RecordNumber = recordNumber, RecordData = new Dictionary(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}'", + 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}'", + 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 || + if (existingAssociation.DestinationEntity != selectedRestEntity.Name || existingAssociation.RestCredentialName != selectedRestCredential) { Logger.LogWarning("ASSOCIATION DEBUG: Associazione non compatibile - Entity: '{FoundEntity}' vs '{ExpectedEntity}', Credential: '{FoundCredential}' vs '{ExpectedCredential}'", @@ -1650,35 +1679,35 @@ public partial class DataCoupler : ComponentBase } } } - - Logger.LogInformation("ASSOCIATION DEBUG: Associazione finale: {Found}. ID: {AssociationId}, DestinationId: '{DestinationId}', IsActive: {IsActive}", + + 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}", + + Logger.LogInformation("ASSOCIATION DEBUG: Record aggiornato con successo tramite associazione: {EntityId} per valore chiave {KeyValue}", existingAssociation.DestinationId, sourceKey); - + transferResults.Add(transferResult); recordNumber++; continue; @@ -1696,8 +1725,8 @@ public partial class DataCoupler : ComponentBase Logger.LogWarning(updateEx, "ASSOCIATION DEBUG: Aggiornamento fallito per associazione {AssociationId} - elimino associazione e creo nuovo record", existingAssociation.Id); goto HandleInvalidAssociation; } - - HandleInvalidAssociation: + + HandleInvalidAssociation: // L'ID di destinazione non esiste più o l'update è fallito - elimina l'associazione non valida try { @@ -1708,26 +1737,26 @@ public partial class DataCoupler : ComponentBase { 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() : + 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)) { @@ -1735,7 +1764,7 @@ public partial class DataCoupler : ComponentBase { // Determina i campi chiave automaticamente var destinationKeyField = GetEntityIdField(); // Campo chiave nella destinazione - + var association = new KeyAssociation { KeyValue = sourceKey, @@ -1754,10 +1783,10 @@ public partial class DataCoupler : ComponentBase SourceType = selectedSourceType }) }; - - Logger.LogInformation("ASSOCIATION DEBUG: Creazione nuova associazione - KeyValue: '{KeyValue}', Entity: '{Entity}', DestinationId: '{DestinationId}', Credential: '{Credential}'", + + 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); } @@ -1767,7 +1796,7 @@ public partial class DataCoupler : ComponentBase // 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 @@ -1786,7 +1815,7 @@ public partial class DataCoupler : ComponentBase errors.Add($"Errore nel trasferimento del record {recordNumber}: {ex.Message}"); Logger.LogError(ex, "Errore nel trasferimento del record {RecordNumber}", recordNumber); } - + transferResults.Add(transferResult); recordNumber++; } @@ -1796,11 +1825,11 @@ public partial class DataCoupler : ComponentBase { var message = $"Trasferimento completato con successo! "; var messageParts = new List(); - + 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"; @@ -1809,12 +1838,12 @@ public partial class DataCoupler : ComponentBase { var message = $"Trasferimento completato con {(duplicateCount > 0 ? "warning e " : "")}errori. "; var messageParts = new List(); - + 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()) { @@ -1824,7 +1853,7 @@ public partial class DataCoupler : ComponentBase transferMessageType = errorCount > 0 ? "error" : "warning"; } - Logger.LogInformation("Trasferimento completato. Inserimenti: {SuccessCount}, Aggiornamenti: {UpdatedCount}, Duplicati: {DuplicateCount}, Errori: {ErrorCount}", + Logger.LogInformation("Trasferimento completato. Inserimenti: {SuccessCount}, Aggiornamenti: {UpdatedCount}, Duplicati: {DuplicateCount}, Errori: {ErrorCount}", successCount, updatedCount, duplicateCount, errorCount); } catch (Exception ex) @@ -1837,7 +1866,8 @@ public partial class DataCoupler : ComponentBase { isTransferringData = false; } - } private async Task>> GetAllRecordsFromSource() + } + private async Task>> GetAllRecordsFromSource() { if (selectedSourceType == "database") { @@ -1847,7 +1877,7 @@ public partial class DataCoupler : ComponentBase { return await GetAllRecordsFromFile(); } - + return new List>(); } @@ -1865,16 +1895,16 @@ public partial class DataCoupler : ComponentBase { 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 @@ -1884,23 +1914,24 @@ public partial class DataCoupler : ComponentBase { 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}", + 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>> GetAllRecordsFromFile() + } + private async Task>> GetAllRecordsFromFile() { if (string.IsNullOrEmpty(selectedSheet) || !fileData.ContainsKey(selectedSheet)) { return new List>(); } - + await Task.CompletedTask; return fileData[selectedSheet]; } @@ -1917,10 +1948,10 @@ public partial class DataCoupler : ComponentBase 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; @@ -1928,8 +1959,8 @@ public partial class DataCoupler : ComponentBase } } - Logger.LogDebug("Record trasformato: {DbColumns} → {RestProperties}", - string.Join(", ", dbRecord.Keys), + Logger.LogDebug("Record trasformato: {DbColumns} → {RestProperties}", + string.Join(", ", dbRecord.Keys), string.Join(", ", restData.Keys)); return restData; @@ -1941,10 +1972,10 @@ public partial class DataCoupler : ComponentBase return null; // Ottieni informazioni sui tipi per fare trasformazioni intelligenti - var dbColumnInfo = databaseTables.ContainsKey(selectedTable) + 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 @@ -1954,19 +1985,19 @@ public partial class DataCoupler : ComponentBase { 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; @@ -1974,7 +2005,7 @@ public partial class DataCoupler : ComponentBase 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)) @@ -2010,9 +2041,9 @@ public partial class DataCoupler : ComponentBase // Common separators to check var separators = new[] { ',', ';', '\t', '|' }; var counts = new Dictionary(); - + bool inQuotes = false; - + // Count separators outside of quotes foreach (char c in line) { @@ -2025,7 +2056,7 @@ public partial class DataCoupler : ComponentBase counts[c] = counts.GetValueOrDefault(c, 0) + 1; } } - + // Return the separator with the highest count, default to comma if (counts.Any()) { @@ -2035,7 +2066,7 @@ public partial class DataCoupler : ComponentBase { return mostCommon.Key; } - } + } return ','; // Default fallback } @@ -2047,11 +2078,11 @@ public partial class DataCoupler : ComponentBase // 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; } @@ -2116,7 +2147,7 @@ public partial class DataCoupler : ComponentBase { 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."); @@ -2148,7 +2179,7 @@ public partial class DataCoupler : ComponentBase isConnectingDatabase = true; databaseErrorMessage = ""; - + try { // Trova la credenziale @@ -2211,17 +2242,17 @@ public partial class DataCoupler : ComponentBase { 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> ?? + databaseTables = schema as Dictionary> ?? (schema != null ? new Dictionary>(schema) : new Dictionary>()); 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."; + // Schema discovery completato senza successo + databaseErrorMessage = "Impossibile rilevare le tabelle del database. Verificare le credenziali di connessione."; } else { @@ -2251,7 +2282,7 @@ public partial class DataCoupler : ComponentBase // 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> ?? new Dictionary>(); @@ -2312,11 +2343,11 @@ public partial class DataCoupler : ComponentBase 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 => + 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"; @@ -2324,7 +2355,7 @@ public partial class DataCoupler : ComponentBase /// /// Verifica se una query è una SELECT query sicura - + /// private bool IsSelectQuery(string query) { @@ -2332,20 +2363,20 @@ public partial class DataCoupler : ComponentBase 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) { @@ -2355,14 +2386,14 @@ public partial class DataCoupler : ComponentBase 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; } @@ -2376,16 +2407,16 @@ public partial class DataCoupler : ComponentBase // 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+", " "); @@ -2398,7 +2429,7 @@ public partial class DataCoupler : ComponentBase private void OnQueryModeChanged(ChangeEventArgs e) { useCustomQuery = (bool)(e.Value ?? false); - + // Reset stato quando cambia modalità if (useCustomQuery) { @@ -2416,7 +2447,7 @@ public partial class DataCoupler : ComponentBase queryColumns.Clear(); showQueryPreview = false; } - + StateHasChanged(); } @@ -2433,7 +2464,7 @@ public partial class DataCoupler : ComponentBase } isValidatingQuery = true; - + try { // Controllo di sicurezza: verifica che sia una SELECT @@ -2445,7 +2476,7 @@ public partial class DataCoupler : ComponentBase } var cleanQuery = CleanQuery(customQuery); - + // Trova la credenziale per determinare il tipo di database var credential = databaseCredentials.FirstOrDefault(c => c.Name == selectedDatabaseCredential); if (credential == null) @@ -2454,28 +2485,28 @@ public partial class DataCoupler : ComponentBase 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 @@ -2506,11 +2537,11 @@ public partial class DataCoupler : ComponentBase 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) @@ -2518,16 +2549,16 @@ public partial class DataCoupler : ComponentBase 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) @@ -2549,7 +2580,7 @@ public partial class DataCoupler : ComponentBase { return databaseType switch { - DatabaseType.SqlServer => $"SELECT TOP {limit} * FROM ({baseQuery}) AS subquery", + 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}", @@ -2587,7 +2618,7 @@ public partial class DataCoupler : ComponentBase return null; } } - + return null; } @@ -2609,7 +2640,7 @@ public partial class DataCoupler : ComponentBase return null; } } - + return null; } @@ -2629,12 +2660,12 @@ public partial class DataCoupler : ComponentBase private async Task> GetTableColumns(string fullTableName, string databaseName, string tableName) { var columns = new List(); - + try { var credential = databaseCredentials.FirstOrDefault(c => c.Name == selectedDatabaseCredential); if (credential == null) return columns; - + var columnsQuery = credential.DatabaseType switch { DatabaseType.SqlServer => $@" @@ -2644,7 +2675,7 @@ public partial class DataCoupler : ComponentBase 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 @@ -2652,29 +2683,29 @@ public partial class DataCoupler : ComponentBase 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) @@ -2682,12 +2713,12 @@ public partial class DataCoupler : ComponentBase 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 @@ -2704,7 +2735,7 @@ public partial class DataCoupler : ComponentBase { Logger.LogError(ex, "Errore nell'ottenere le colonne per la tabella {TableName}", fullTableName); } - + return columns; } @@ -2717,14 +2748,14 @@ public partial class DataCoupler : ComponentBase 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); @@ -2736,7 +2767,7 @@ public partial class DataCoupler : ComponentBase Logger.LogError(ex, "Errore nella connessione con lo schema selezionato"); databaseErrorMessage = $"Errore nella connessione al database {selectedSchema}: {ex.Message}"; } - + StateHasChanged(); } @@ -2750,7 +2781,7 @@ public partial class DataCoupler : ComponentBase isLoadingSchemas = true; availableSchemas.Clear(); - + try { // Prova a ottenere tutti gli schemi/database disponibili @@ -2759,7 +2790,7 @@ public partial class DataCoupler : ComponentBase try { var allSchemas = await currentDatabaseManager.GetDatabaseSchemaAsync(); - + if (allSchemas != null) { // Estrai i nomi degli schemi dalle chiavi delle tabelle @@ -2769,11 +2800,11 @@ public partial class DataCoupler : ComponentBase .Distinct() .OrderBy(schema => schema) .ToList(); - + if (schemaNames.Any()) { availableSchemas.AddRange(schemaNames); - Logger.LogInformation("Rilevati {SchemaCount} schemi dalle tabelle: {Schemas}", + Logger.LogInformation("Rilevati {SchemaCount} schemi dalle tabelle: {Schemas}", schemaNames.Count, string.Join(", ", schemaNames)); return; } @@ -2824,10 +2855,10 @@ public partial class DataCoupler : ComponentBase { 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 schemas = results.Select(row => { var firstValue = row.Values.FirstOrDefault(); return firstValue?.ToString() ?? ""; @@ -2839,7 +2870,7 @@ public partial class DataCoupler : ComponentBase if (schemas.Any()) { availableSchemas.AddRange(schemas); - Logger.LogInformation("Caricati {SchemaCount} database/schemi via query diretta per {DatabaseType}: {Schemas}", + Logger.LogInformation("Caricati {SchemaCount} database/schemi via query diretta per {DatabaseType}: {Schemas}", schemas.Count, credential.DatabaseType, string.Join(", ", schemas)); } } @@ -2885,11 +2916,11 @@ public partial class DataCoupler : ComponentBase databaseErrorMessage = "Nessun database selezionato"; return; } - + showDatabaseSelectionModal = false; - + Logger.LogInformation("Database selezionato: {DatabaseName}. Riconnessione in corso...", selectedDatabase); - + // Riconnessione al database selezionato await ConnectToDatabaseWithSpecificDatabase(selectedDatabase); } @@ -2913,16 +2944,16 @@ public partial class DataCoupler : ComponentBase { try { - Logger.LogInformation("Verifica database specificato - Tipo: {DatabaseType}, DatabaseName: '{DatabaseName}', Connection: {ConnectionString}", + 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) { @@ -2930,18 +2961,18 @@ public partial class DataCoupler : ComponentBase 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}", + + 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 } @@ -2965,9 +2996,9 @@ public partial class DataCoupler : ComponentBase 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(); } @@ -2991,15 +3022,15 @@ public partial class DataCoupler : ComponentBase 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)", + + Logger.LogInformation("Trovati {TotalDatabases} database, filtrati a {FilteredDatabases} (esclusi quelli di sistema)", allDatabases.Count, availableDatabases.Count); } catch (Exception ex) @@ -3025,7 +3056,7 @@ public partial class DataCoupler : ComponentBase } var databaseType = credential.DatabaseType; - + // Filtri per SQL Server if (databaseType == DatabaseType.SqlServer) { @@ -3034,29 +3065,29 @@ public partial class DataCoupler : ComponentBase "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 } @@ -3066,8 +3097,34 @@ public partial class DataCoupler : ComponentBase 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"; @@ -3075,14 +3132,14 @@ public partial class DataCoupler : ComponentBase } 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); } @@ -3105,54 +3162,54 @@ public partial class DataCoupler : ComponentBase sourceKeyField = ""; suggestedPrimaryKey = ""; requiresManualKeySelection = true; - + // Pattern comuni per identificare possibili chiavi primarie var keyPatterns = new[] { - "id", "ID", "Id", + "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 => + 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 => + 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 => + 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 @@ -3180,57 +3237,57 @@ public partial class DataCoupler : ComponentBase sourceKeyField = ""; suggestedPrimaryKey = ""; requiresManualKeySelection = true; - + // Pattern comuni per identificare possibili chiavi primarie nei file var keyPatterns = new[] { - "id", "ID", "Id", + "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 => + 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 => + 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 => + 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 @@ -3251,13 +3308,13 @@ public partial class DataCoupler : ComponentBase { var uniqueName = baseName; var counter = 1; - + while (await ProfileService.GetProfileByNameIncludingInactiveAsync(uniqueName) != null) { uniqueName = $"{baseName} ({counter})"; counter++; } - + return uniqueName; } } diff --git a/Data_Coupler/Pages/DataCoupler_temp.cs b/Data_Coupler/Pages/DataCoupler_temp.cs deleted file mode 100644 index e69de29..0000000 diff --git a/Data_Coupler/wwwroot/data/credentials.db b/Data_Coupler/wwwroot/data/credentials.db index cbd53c8..b1ecb44 100644 Binary files a/Data_Coupler/wwwroot/data/credentials.db and b/Data_Coupler/wwwroot/data/credentials.db differ diff --git a/Data_Coupler/wwwroot/data/credentials.db-shm b/Data_Coupler/wwwroot/data/credentials.db-shm index a814b92..7f3662a 100644 Binary files a/Data_Coupler/wwwroot/data/credentials.db-shm and b/Data_Coupler/wwwroot/data/credentials.db-shm differ diff --git a/Data_Coupler/wwwroot/data/credentials.db-wal b/Data_Coupler/wwwroot/data/credentials.db-wal index 7c12581..fd11c4d 100644 Binary files a/Data_Coupler/wwwroot/data/credentials.db-wal and b/Data_Coupler/wwwroot/data/credentials.db-wal differ diff --git a/TestCredentialDatabaseName/Program.cs b/TestCredentialDatabaseName/Program.cs new file mode 100644 index 0000000..54e7af4 --- /dev/null +++ b/TestCredentialDatabaseName/Program.cs @@ -0,0 +1,53 @@ +using CredentialManager.Data; +using CredentialManager.Models; +using CredentialManager.Services; +using Microsoft.EntityFrameworkCore; + +Console.WriteLine("🧪 Testing SourceDatabaseName retrieval from credentials..."); + +// Configurazione del database temporaneo +var options = new DbContextOptionsBuilder() + .UseSqlite("Data Source=test_credential_db.db") + .Options; + +using var context = new CredentialDbContext(options); +await context.Database.EnsureCreatedAsync(); + +var credentialService = new CredentialService(context); + +// Test 1: Crea una credenziale database con nome database +var testCredential = new DatabaseCredential +{ + Name = "TestDatabaseCredential", + DatabaseType = "SqlServer", + Host = "localhost", + Port = 1433, + DatabaseName = "MyProductionDB", + Username = "testuser", + Password = "testpassword" +}; + +Console.WriteLine($"📝 Creando credenziale con DatabaseName: {testCredential.DatabaseName}"); +var credentialId = await credentialService.SaveDatabaseCredentialAsync(testCredential); +Console.WriteLine($"✅ Credenziale salvata con ID: {credentialId}"); + +// Test 2: Recupera la credenziale +var retrievedCredential = await credentialService.GetDatabaseCredentialAsync(credentialId); +Console.WriteLine($"✅ Credenziale recuperata: {retrievedCredential?.Name}"); +Console.WriteLine($" DatabaseName: {retrievedCredential?.DatabaseName}"); + +// Test 3: Simula il recupero del database name come farebbe ProfileSaver +if (retrievedCredential != null && !string.IsNullOrEmpty(retrievedCredential.DatabaseName)) +{ + Console.WriteLine($"✅ SUCCESSO: DatabaseName recuperato dalle credenziali: {retrievedCredential.DatabaseName}"); +} +else +{ + Console.WriteLine("❌ ERRORE: DatabaseName non recuperato dalle credenziali"); +} + +// Pulizia +await context.Database.EnsureDeletedAsync(); +Console.WriteLine("🧹 Database temporaneo eliminato"); + +Console.WriteLine("\n🎯 Test completato con successo!"); diff --git a/TestCredentialDatabaseName/TestCredentialDatabaseName.csproj b/TestCredentialDatabaseName/TestCredentialDatabaseName.csproj new file mode 100644 index 0000000..e696ab1 --- /dev/null +++ b/TestCredentialDatabaseName/TestCredentialDatabaseName.csproj @@ -0,0 +1,15 @@ + + + + Exe + net9.0 + enable + enable + + + + + + + + diff --git a/TestSourceDatabaseName/Program.cs b/TestSourceDatabaseName/Program.cs new file mode 100644 index 0000000..9289edb --- /dev/null +++ b/TestSourceDatabaseName/Program.cs @@ -0,0 +1,62 @@ +using CredentialManager.Data; +using CredentialManager.Models; +using CredentialManager.Services; +using Microsoft.EntityFrameworkCore; + +Console.WriteLine("🧪 Testing SourceDatabaseName database persistence..."); + +// Configurazione del database temporaneo +var options = new DbContextOptionsBuilder() + .UseSqlite("Data Source=test_sourcedatabase.db") + .Options; + +using var context = new CredentialDbContext(options); +await context.Database.EnsureCreatedAsync(); + +var profileService = new DataCouplerProfileService(context); + +// Test: Creazione e salvataggio di un profilo con SourceDatabaseName +var testProfile = new DataCouplerProfile +{ + Name = "Test Profile DB", + Description = "Test per verificare il salvataggio del SourceDatabaseName", + SourceType = "database", + SourceDatabaseName = "MyProductionDatabase", + SourceSchema = "dbo", + SourceTable = "customers", + DestinationType = "rest", + DestinationEndpoint = "/api/customers", + CreatedBy = "TestUser" +}; + +Console.WriteLine($"📝 Creando profilo con SourceDatabaseName: {testProfile.SourceDatabaseName}"); + +// Salvataggio nel database +var savedProfile = await profileService.SaveProfileAsync(testProfile); +Console.WriteLine($"✅ Profilo salvato con ID: {savedProfile.Id}"); + +// Recupero dal database +var retrievedProfile = await profileService.GetProfileByIdAsync(savedProfile.Id); +Console.WriteLine($"✅ Profilo recuperato dal database"); + +// Verifica che il SourceDatabaseName sia stato salvato e recuperato correttamente +if (retrievedProfile != null && retrievedProfile.SourceDatabaseName == testProfile.SourceDatabaseName) +{ + Console.WriteLine($"✅ SUCCESSO: SourceDatabaseName salvato e recuperato correttamente: {retrievedProfile.SourceDatabaseName}"); +} +else +{ + Console.WriteLine($"❌ ERRORE: SourceDatabaseName non salvato correttamente"); + Console.WriteLine($" Originale: {testProfile.SourceDatabaseName}"); + Console.WriteLine($" Recuperato: {retrievedProfile?.SourceDatabaseName ?? "NULL"}"); +} + +// Test conversione DTO +var dto = profileService.ToDto(retrievedProfile!); +Console.WriteLine($"✅ DTO convertito con SourceDatabaseName: {dto.SourceDatabaseName}"); + +// Pulizia +await context.Database.EnsureDeletedAsync(); +Console.WriteLine("🧹 Database temporaneo eliminato"); + +Console.WriteLine("\n🎯 Test completato con successo!"); diff --git a/TestSourceDatabaseName/TestSourceDatabaseName.csproj b/TestSourceDatabaseName/TestSourceDatabaseName.csproj new file mode 100644 index 0000000..e696ab1 --- /dev/null +++ b/TestSourceDatabaseName/TestSourceDatabaseName.csproj @@ -0,0 +1,15 @@ + + + + Exe + net9.0 + enable + enable + + + + + + + +