From 7d2961702c2dc89f0b9f2415c699c258d5a0ee9b Mon Sep 17 00:00:00 2001 From: Alessio Dal Santo Date: Sat, 5 Jul 2025 18:10:09 +0200 Subject: [PATCH] feat: Aggiunta gestione nome database sorgente nei profili DataCoupler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✨ Nuove funzionalità: - Aggiunto campo SourceDatabaseName nella tabella DataCouplerProfiles - Implementato recupero automatico del nome database dalle credenziali - Migliorata applicazione profili con supporto database specifico - Aggiornata logica di connessione database con selezione database 🔧 Modifiche tecniche: - Aggiunta migration per colonna SourceDatabaseName - Estesi modelli DataCouplerProfile e DataCouplerProfileDto - Aggiornato DataCouplerProfileService per gestire nuovo campo - Modificato ProfileSaver per recupero automatico database name - Implementato metodo ConnectToDatabaseWithSpecificDatabase 🐛 Correzioni: - Migliorata gestione connessioni database multi-database - Corretta formattazione codice e spaziature - Rimosse linee vuote eccessive nel codice sorgente 🧪 --- Components/ProfileQuickActions.razor | 0 Components/ProfileQuickActions.razor.cs | 0 Components/ProfileSaver.razor | 20 + Components/ProfileSaver.razor.cs | 36 + ...20_AddSourceDatabaseNameColumn.Designer.cs | 337 ++++++++ ...50704135720_AddSourceDatabaseNameColumn.cs | 29 + .../CredentialDbContextModelSnapshot.cs | 4 + .../Models/DataCouplerProfile.cs | 3 + .../Models/DataCouplerProfileDto.cs | 1 + .../Services/DataCouplerProfileService.cs | 2 + CredentialManager/design_time_temp.db | Bin 118784 -> 118784 bytes Data_Coupler/Pages/DataCoupler.razor | 1 + Data_Coupler/Pages/DataCoupler.razor.cs | 791 ++++++++++-------- Data_Coupler/Pages/DataCoupler_temp.cs | 0 Data_Coupler/wwwroot/data/credentials.db | Bin 126976 -> 126976 bytes Data_Coupler/wwwroot/data/credentials.db-shm | Bin 32768 -> 32768 bytes Data_Coupler/wwwroot/data/credentials.db-wal | Bin 988832 -> 1137152 bytes TestCredentialDatabaseName/Program.cs | 53 ++ .../TestCredentialDatabaseName.csproj | 15 + TestSourceDatabaseName/Program.cs | 62 ++ .../TestSourceDatabaseName.csproj | 15 + 21 files changed, 1002 insertions(+), 367 deletions(-) delete mode 100644 Components/ProfileQuickActions.razor delete mode 100644 Components/ProfileQuickActions.razor.cs create mode 100644 CredentialManager/Migrations/20250704135720_AddSourceDatabaseNameColumn.Designer.cs create mode 100644 CredentialManager/Migrations/20250704135720_AddSourceDatabaseNameColumn.cs delete mode 100644 Data_Coupler/Pages/DataCoupler_temp.cs create mode 100644 TestCredentialDatabaseName/Program.cs create mode 100644 TestCredentialDatabaseName/TestCredentialDatabaseName.csproj create mode 100644 TestSourceDatabaseName/Program.cs create mode 100644 TestSourceDatabaseName/TestSourceDatabaseName.csproj 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 88eb0a9f590f42cca4f13270f826361c638587ac..cf8a0be3a8483a327179fabca60c51c74831438a 100644 GIT binary patch delta 280 zcmZozz}~QceS)+g3j+g#ED*zh^h6zFMwX2U9r79`3Wmm3rsh_rmU@=PrY6P)#@Yr3 zRt5&VK(#FVG7S8?_`CT7_+>T=3h?lo7_l%XiW?ainHrcIm>3$Hnj0C!JEo)r=a&{G zr@ADTBqk*mr}`!4raI^6l;-AH>KW)6Ob*cB!o$LE$iRPqe;R)Rzah{lMSguf=0rx~ zO=4+Iu%Dh_&$vvam4SacKObK%?{nTno|8PP+=Yy-+oc>Bvl+QM{e{@Yjg1*6xo%(Y d$QZ}Wp`ZkD+2ps}a?|&?GU{)4a%J4t1pw&iN96zj delta 160 zcmZozz}~QceS)+gGXn#IED*zh)I=R)M&^wP9r9|%3I-Ndra)w>XK89~XlY`hZD3$! zV89Dh%gi6Zz`u*Xn?GQ)pnwzqo6YQra*fTB@S<1ja zou7{{m-jhuBF{-4RqjH@rQ4+(7_%8CE>Pg=)E8nGH#TOR#JauTkui>WJFhF_=Pm$Q CD 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 cbd53c86412a20c68f938cea91aab609f83fe84e..b1ecb44cc386f4495a07794c52ec21c25409900d 100644 GIT binary patch delta 4307 zcma)93v3j}8J?fJ^ZD$%8yi9`*El!^6BF=yW@q+wQ1Ed?SWwwSsj5_v;v+sG9tL7e z)K;SQ(WYvJ(v+N(l~PqgP1BU%)c8RRiPKP8wW>q0ydiE^Xyal*b5(QL1RV zMs!PECB6uW2w6W~g7EA37~YDPjMvtLF<1S*WSvS>jTnSerWq>P5%2AN^uXc%CsG@e z1IeAqL#eK0Un+56-{HRfmO>Ogv$cI9?8g-0^Z0q(gQewK{_&pmUuuHj z_~jj~2Wy3HOYma|597!15Ag~7!uZ19J%bm$8t{b{pkeoU%6*<3ANy-}&HC`mXi0cu zFtR!PjmXMKD0~h78BgKw;;-N@gj3kWb7-W85xco zi|i2!U#-qGO)Y%xKmJVYUe&H9Ut)oydmAy@2y+;uW{jH(H%!6_ML+QtV2W-Ki&`cc z_Y`23Vo*&piH=?oCN^c>p1m*g*1gKXrM|ewd6vR;LUj}U)Dw*hj}nzq(?G8xZv?7P z!!lJ34SORH#bg@Q2Q3Yq@!h#E^(4Rq2IK_dzymTu@e`uThUf=9*)LqGEr;DVDu z!c<+Q=)!yw)Ur6$DLU^hU@E4`EzK~{TiybO!d0RWqSmm4Ek7`>rgwERuW-qUphO8%FhwjSaLRk;oj|tJsDDN_!y#GOw6n zm6w2A%GonpV43}z0-Hg%VLhaZ3I$gjFEFB&1IQ)eAUoYE5YDZKO<3rp&FK`kZ#@9p zltP81(^P|b;ko{_)0RMcp&2@BjtDqj2shJQ0w+Dp4G;CQ&X;h{J_w!md&^*Nk#VwS zn5M_>Y&o%9j2=m^ z%;Ux5_T3iP)>gokQ?LxpqP!d}Tgs&CezXEM+1DO~Jw=|E1)O;iovZDVnipGPtcVqv z6c*9T&gJ=haja;uUs(wwMTg3CVJ0o#n#&}q^QnhmwCGS(*lH3(EvJslC6Qt>&PG5@a81Y@k6C$UxLp8d88%k8n%B|By2G2(G(E-58P6K$|LygKi!MBEOy!?q$VWPjqO zCzfno()H)sVN=VIi0_XmihjKyIve>oo8Gr8Z@_PZ?^paS@E89(V(ItCkH5V5C`u>h z&P{|+I{A6Sh8VQk%_g)JsvsJe)1NdCmuHtav5&Wj~gWud3k(t@qWS?jSzx^*2HrrQq*j4D=Y-_c6d7{mAUCGY(43Vg_>mWQiV?YuL zlgnwZqPv=gS{_|;Nw?E2!p}djpmVV(9q~B}<1TTUB<}PUXe$ABrW1>L_%1uRIAO5D zuKO%JjGdy?kw=c(9R#+QpgBF>!&YH{?EZ6f-60}(H41wR9Vw zgu82%)NDC^Fy_&dvXhBS*OM_AEuuw^CEW5TOu30g#`o|VQLVq4w(@GkZCDH23yZ55 zf}9b?tKP^JVh>p&mPXc=6e>B`Sj2;j{LCZS82?O3xp7AxM!do-Hw{U*o>>Q-&v$@7 z?5{eHqT9niuAZzqKUvcOr>k1i1Hn2}j~YXZ8Y7DVp@yTY>Juo`7($;*9U5q2z1f?M zeY6W+vZuPCW`lcgVp@z*#nLp5QzMb=?@b)&O{E7aJisb*V7wgc5f@BVV}vRO7bCYt zb6{Uv75J2+87yj%ntlE8`b1o4<|?M8F{4?J=B(7=Jd9Zw!k^?%uZrVEBE`wnjH z-~Ci_z#i>_y2ysz`}XbLzw6O~{#0tfPIkk7F*T|r#x#X8qH|hONK__nHR4LcMNs9` zncUt5--f7r-yxbJ(n$=YlmG69Xw7|3BW&Ynv@dckk_`VU+*17{JU#j89@q<2-|Ai% ztYZwGdw6mr319S8HZ{vDW9C-%{q|G}+9x-p;6jzSCJrGyEiQ^D@Q@hD4&t7rzJz;C zbgx?FqzB8_i+HRkT6eE%MJ5OA=|Na7&b^NCC-?(=8($x<9r_b)4fp_jLHAkVJ_GL4 zU*+D5uOR%9kbMtd5z-eiuk)EC-?z~#&D q^5xIUUPSnx_Ec~b_{0J0&`2Pp@kKoGy delta 3845 zcmb7HdvH`&89!g{hHN%>_as1|B%~}!paGJbd+xdCJ{D*a7n)2M%PV$t$k+t3wNMHO zbjIq8Vfn++X_ewtj@6FED(wgqlZF!Or0Mey{X@r@j*gu^Oq>dKq)mls$v_xAch5eu zkI`|nkL-8O`JM0kea}5MRdH;p;)(j2AVO$`cx4`SCtE25M+d&WVl|EGZp>sc!_aJl zGG?)Lx*pT@u-L2MZ3zDcKZduB)l`2Dd)wMCh7>%2@T>R~9uTq}cx<5KYf%V{y?uYn zp&EQ1Eyp_$einZbpTZTREx@73qR=fjVM z_X!0)sPdu{3q5Nt0_VJ%$=X(CX~Zyf)1hc=h0;}K=mu-ktv17r5+`OFF=lFv>5fj& z^Zo&qQO#!5(hY*n`T}oat}6Q4ok~ZEqmJeh!VEMiPnw!xxu)Y1blw+K3n?-w&B_oy^#~?#4%jQL9fdz zUClIYYFie1H6tJnF)hO*T2njd(WKoNb z&HcMPx^uw{ilBE;`PIOO%H_!I@GW(=>eJA*%9-FlE3TCN4c-@vXHtK{8~KmqaIy)b zWepAWq&^<+szk}b`wXF;H~nC=%Fmem}XK+n8^x)io^_EvxTE{OT6;p zEwe-%`x}jYhX)Vvw;N%vHa{LEeTMs=_#Mrl zGcCd&B9dma z(1?HB4E^5p?>f{uUVE};?it6lYq~zfzdoNWGYdo1R0qu z<4^ePo?Q#0d2V5%0y&m2wAj+r5_7%j{VVxM8wB`;budzJt)HJJE1@ydWa`RmxAa2X zt9~HBXIo)!0fe8?gxaOt9$PZ=Mfof~m@Nv@MVB<_Rok-M$Gg18A8O+Z z8$rusLUsgZ6ee%3S)>#>(FmVcTXSe}uC~LjJeyJ-fss zg>R~#Rb3B#Qh7Ddsca~VfrX6nO~HRu{C&Z+XqG*>Y9&&&Gj~7##De}jW@LA89IKd_ zY~zq|^ix}8>Seni4qeS}CN^_Rolo}}dH&~)s2s^}ChyLv)4|^-u&Z@`rpkUvNeLIH zmb{gN=>U7&fL{KQ17FFrNA@z?DS1BK=A=H)xw4XN$mtX3x7~tDmTHBfiIi}3wo6L9 zvoVR+H6QOI2D@tK+voZ{$hHab&dt^P2OIg57W70Sx6zZ?gNRQXqAFaQSPZWB+E5*T z%a~7lb`~I3VL)e>#F!w_Pf+Lyh<+6xe}}?e$dQ%WDh#896goe%&KKooT3^JKKPQOcpg0z?BZTDY`SmcG(?leVoGHYRReUr%Zpfpf8=za1d^e{Ef2vm$ zt7;3XxVaJXCpS5qn5DugJGV&Ov5f`wN{%tClr@a{$3*_@~X`L)8|ihLO_K`KZ^8+|Daw_`^T$0VI-8}h2OIg7DLLPOC^5AApm=Iq<2yDTP8>PPXSTqZVHzbyjF6aa z`!g~z>~ur2+w{~DwPK8DqRBbbVn;d>yaYv#m=qZuN8O?!NHD@3XnpFx=8NaW%0mGGeYJ9SmnSK-Wf-2glU zCr?L1-)Uc1v6NEye);%`LHM#V9!|iGPzdl8!c+Kl{EYai@dM0{4MUg|TdJ&3;vi#P znybf%8#Ac?L&Me$J~a%@0_!HiH}N$*H&%1(U3_v|aCtxxfg;|@6%d~x!9Cy`2;aan z__Ema7tG&%8iG+JsMLbbbmlX?^%lan3h;tHUJwFGC8Re^BRq|NhNp!Ao-}}8cm+cD zWn59DyYQkOGngjAT0BYFok~}pZTalxYdtHp{xe=1QkH=Xo|Ni+Y1RGD^#1=!MIgQD uI>OiSTX;gKegX4|8K^d804hrq0>yz*CI}3r#H!K&LxV>LcMl#(1pWi8BAQ|V diff --git a/Data_Coupler/wwwroot/data/credentials.db-shm b/Data_Coupler/wwwroot/data/credentials.db-shm index a814b927ca7e74c917c7890b83ce02fcf9474b7b..7f3662ad66111789d6f22b027d440820c356b5f1 100644 GIT binary patch delta 625 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 delta 31743 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 YCEN(7ZE=XQ#UZ^dFo*)F^T)RSKO_N<#sB~S 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 + + + + + + + +