diff --git a/Components/ProfileSaver.razor b/Components/ProfileSaver.razor index 52142ad..2699635 100644 --- a/Components/ProfileSaver.razor +++ b/Components/ProfileSaver.razor @@ -1,4 +1,6 @@ @* Componente per salvare la configurazione corrente come profilo *@ +@using System.IO +
@@ -57,6 +59,10 @@
Fonte: @GetSourceSummary()
+ @if (!string.IsNullOrEmpty(SourceCredentialName)) + { + Credenziali: @SourceCredentialName
+ } @if (!string.IsNullOrEmpty(SourceSchema)) { Schema: @SourceSchema
@@ -65,9 +71,17 @@ { Tabella: @SourceTable } + @if (!string.IsNullOrEmpty(SourceFilePath)) + { + File: @Path.GetFileName(SourceFilePath) + }
Destinazione: @GetDestinationSummary()
+ @if (!string.IsNullOrEmpty(DestinationCredentialName)) + { + Credenziali: @DestinationCredentialName
+ } @if (!string.IsNullOrEmpty(DestinationSchema)) { Schema: @DestinationSchema
@@ -85,10 +99,32 @@ @if (FieldMappings != null && FieldMappings.Any()) {
- - - @FieldMappings.Count mapping dei campi configurati - +
+
+ + + @FieldMappings.Count mapping dei campi configurati + +
+
+ @if (UseRecordAssociations) + { + + Smart Update attivo + @if (!string.IsNullOrEmpty(SourceKeyField)) + { + (Chiave: @SourceKeyField) + } + + } + else + { + + Solo inserimenti + + } +
+
}
diff --git a/Components/ProfileSaver.razor.cs b/Components/ProfileSaver.razor.cs index a776486..2077d4a 100644 --- a/Components/ProfileSaver.razor.cs +++ b/Components/ProfileSaver.razor.cs @@ -9,15 +9,19 @@ public partial class ProfileSaver [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? SourceSchema { get; set; } [Parameter] public string? SourceTable { get; set; } [Parameter] public string? SourceFilePath { get; set; } [Parameter] public string DestinationType { get; set; } = ""; [Parameter] public int? DestinationCredentialId { get; set; } + [Parameter] public string? DestinationCredentialName { get; set; } [Parameter] public string? DestinationSchema { get; set; } [Parameter] public string? DestinationTable { get; set; } [Parameter] public string? DestinationEndpoint { get; set; } [Parameter] public List? FieldMappings { get; set; } + [Parameter] public string? SourceKeyField { get; set; } + [Parameter] public bool UseRecordAssociations { get; set; } [Parameter] public EventCallback OnProfileSaved { get; set; } private bool ShowSaveForm { get; set; } = false; @@ -53,15 +57,19 @@ public partial class ProfileSaver Description = ProfileData.Description, SourceType = SourceType, SourceCredentialId = SourceCredentialId, + SourceCredentialName = SourceCredentialName, SourceSchema = SourceSchema, SourceTable = SourceTable, SourceFilePath = SourceFilePath, DestinationType = DestinationType, DestinationCredentialId = DestinationCredentialId, + DestinationCredentialName = DestinationCredentialName, DestinationSchema = DestinationSchema, DestinationTable = DestinationTable, DestinationEndpoint = DestinationEndpoint, - FieldMappings = FieldMappings + FieldMappings = FieldMappings, + SourceKeyField = SourceKeyField, + UseRecordAssociations = UseRecordAssociations }; await OnProfileSaved.InvokeAsync(profileDto); diff --git a/CredentialManager/Migrations/20250703085823_AddProfileKeyFieldsAndAssociations.Designer.cs b/CredentialManager/Migrations/20250703085823_AddProfileKeyFieldsAndAssociations.Designer.cs new file mode 100644 index 0000000..52ef9f7 --- /dev/null +++ b/CredentialManager/Migrations/20250703085823_AddProfileKeyFieldsAndAssociations.Designer.cs @@ -0,0 +1,333 @@ +// +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("20250703085823_AddProfileKeyFieldsAndAssociations")] + partial class AddProfileKeyFieldsAndAssociations + { + /// + 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("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/20250703085823_AddProfileKeyFieldsAndAssociations.cs b/CredentialManager/Migrations/20250703085823_AddProfileKeyFieldsAndAssociations.cs new file mode 100644 index 0000000..232d32c --- /dev/null +++ b/CredentialManager/Migrations/20250703085823_AddProfileKeyFieldsAndAssociations.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CredentialManager.Migrations +{ + /// + public partial class AddProfileKeyFieldsAndAssociations : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SourceKeyField", + table: "DataCouplerProfiles", + type: "TEXT", + maxLength: 200, + nullable: true); + + migrationBuilder.AddColumn( + name: "UseRecordAssociations", + table: "DataCouplerProfiles", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "SourceKeyField", + table: "DataCouplerProfiles"); + + migrationBuilder.DropColumn( + name: "UseRecordAssociations", + table: "DataCouplerProfiles"); + } + } +} diff --git a/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs b/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs index 739f22a..80c149e 100644 --- a/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs +++ b/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs @@ -186,6 +186,10 @@ namespace CredentialManager.Migrations .HasMaxLength(500) .HasColumnType("TEXT"); + b.Property("SourceKeyField") + .HasMaxLength(200) + .HasColumnType("TEXT"); + b.Property("SourceSchema") .HasMaxLength(200) .HasColumnType("TEXT"); @@ -199,6 +203,9 @@ namespace CredentialManager.Migrations .HasMaxLength(20) .HasColumnType("TEXT"); + b.Property("UseRecordAssociations") + .HasColumnType("INTEGER"); + b.HasKey("Id"); b.HasIndex("CreatedAt"); diff --git a/CredentialManager/Models/DataCouplerProfile.cs b/CredentialManager/Models/DataCouplerProfile.cs index 047032a..a188225 100644 --- a/CredentialManager/Models/DataCouplerProfile.cs +++ b/CredentialManager/Models/DataCouplerProfile.cs @@ -54,6 +54,12 @@ public class DataCouplerProfile [MaxLength(4000)] public string? FieldMappingJson { get; set; } + // Configurazione chiave sorgente e associazioni + [MaxLength(200)] + public string? SourceKeyField { get; set; } + + public bool UseRecordAssociations { get; set; } = false; + // Metadati [MaxLength(100)] public string? CreatedBy { get; set; } diff --git a/CredentialManager/Models/DataCouplerProfileDto.cs b/CredentialManager/Models/DataCouplerProfileDto.cs index 86f7b2d..e458c57 100644 --- a/CredentialManager/Models/DataCouplerProfileDto.cs +++ b/CredentialManager/Models/DataCouplerProfileDto.cs @@ -12,6 +12,7 @@ public class DataCouplerProfileDto // Informazioni sorgente public string SourceType { get; set; } = string.Empty; public int? SourceCredentialId { get; set; } + public string? SourceCredentialName { get; set; } public string? SourceSchema { get; set; } public string? SourceTable { get; set; } public string? SourceFilePath { get; set; } @@ -19,12 +20,17 @@ public class DataCouplerProfileDto // Informazioni destinazione public string DestinationType { get; set; } = string.Empty; public int? DestinationCredentialId { get; set; } + public string? DestinationCredentialName { get; set; } public string? DestinationSchema { get; set; } public string? DestinationTable { get; set; } public string? DestinationEndpoint { get; set; } // Mapping dei campi public List? FieldMappings { get; set; } + + // Configurazione chiave sorgente e associazioni + public string? SourceKeyField { get; set; } + public bool UseRecordAssociations { get; set; } } /// diff --git a/CredentialManager/Services/CredentialService.cs b/CredentialManager/Services/CredentialService.cs index c449b24..98aa569 100644 --- a/CredentialManager/Services/CredentialService.cs +++ b/CredentialManager/Services/CredentialService.cs @@ -39,6 +39,9 @@ public interface ICredentialService Task DeleteCredentialAsync(int id); Task DeleteCredentialAsync(string name); Task> GetCredentialNamesAsync(CredentialType? type = null); + + // Helper methods to get credential ID by name + Task GetCredentialIdByNameAsync(string name, CredentialType type); } /// @@ -960,5 +963,27 @@ public class CredentialService : ICredentialService credentialValue.Contains("*** ERRORE DECRITTOGRAFIA ***"); } + /// + /// Ottiene l'ID di una credenziale per nome e tipo + /// + /// Nome della credenziale + /// Tipo della credenziale + /// ID della credenziale se trovata, null altrimenti + public async Task GetCredentialIdByNameAsync(string name, CredentialType type) + { + try + { + var entity = await _context.Credentials + .FirstOrDefaultAsync(c => c.Name == name && c.Type == type.ToString() && c.IsActive); + + return entity?.Id; + } + catch (Exception ex) + { + _logger.LogError(ex, "Errore nel recuperare l'ID della credenziale: {Name}, Tipo: {Type}", name, type); + return null; + } + } + #endregion } diff --git a/CredentialManager/Services/DataCouplerProfileService.cs b/CredentialManager/Services/DataCouplerProfileService.cs index 1dbc0d6..98c5053 100644 --- a/CredentialManager/Services/DataCouplerProfileService.cs +++ b/CredentialManager/Services/DataCouplerProfileService.cs @@ -31,6 +31,17 @@ public class DataCouplerProfileService : IDataCouplerProfileService .ToListAsync(); } + /// + /// Ottiene tutti i profili per nome (inclusi quelli inattivi) + /// + public async Task GetProfileByNameIncludingInactiveAsync(string name) + { + return await _context.DataCouplerProfiles + .Include(p => p.SourceCredential) + .Include(p => p.DestinationCredential) + .FirstOrDefaultAsync(p => p.Name.ToLower() == name.ToLower()); + } + /// /// Ottiene un profilo per ID /// @@ -80,8 +91,12 @@ public class DataCouplerProfileService : IDataCouplerProfileService throw new InvalidOperationException($"Profilo con ID {profile.Id} non trovato"); } - // Aggiorna le proprietà - existingProfile.Name = profile.Name; + // Aggiorna le proprietà (evita di aggiornare il nome se è uguale per evitare unique constraint) + if (!string.Equals(existingProfile.Name, profile.Name, StringComparison.OrdinalIgnoreCase)) + { + existingProfile.Name = profile.Name; + } + existingProfile.Description = profile.Description; existingProfile.SourceType = profile.SourceType; existingProfile.SourceCredentialId = profile.SourceCredentialId; @@ -94,6 +109,9 @@ public class DataCouplerProfileService : IDataCouplerProfileService existingProfile.DestinationTable = profile.DestinationTable; existingProfile.DestinationEndpoint = profile.DestinationEndpoint; existingProfile.FieldMappingJson = profile.FieldMappingJson; + existingProfile.SourceKeyField = profile.SourceKeyField; + existingProfile.UseRecordAssociations = profile.UseRecordAssociations; + existingProfile.IsActive = profile.IsActive; await _context.SaveChangesAsync(); return existingProfile; @@ -195,15 +213,19 @@ public class DataCouplerProfileService : IDataCouplerProfileService Description = profile.Description, SourceType = profile.SourceType, SourceCredentialId = profile.SourceCredentialId, + SourceCredentialName = profile.SourceCredential?.Name, SourceSchema = profile.SourceSchema, SourceTable = profile.SourceTable, SourceFilePath = profile.SourceFilePath, DestinationType = profile.DestinationType, DestinationCredentialId = profile.DestinationCredentialId, + DestinationCredentialName = profile.DestinationCredential?.Name, DestinationSchema = profile.DestinationSchema, DestinationTable = profile.DestinationTable, DestinationEndpoint = profile.DestinationEndpoint, - FieldMappings = DeserializeFieldMappings(profile.FieldMappingJson) + FieldMappings = DeserializeFieldMappings(profile.FieldMappingJson), + SourceKeyField = profile.SourceKeyField, + UseRecordAssociations = profile.UseRecordAssociations }; } @@ -228,6 +250,8 @@ public class DataCouplerProfileService : IDataCouplerProfileService DestinationTable = dto.DestinationTable, DestinationEndpoint = dto.DestinationEndpoint, FieldMappingJson = SerializeFieldMappings(dto.FieldMappings), + SourceKeyField = dto.SourceKeyField, + UseRecordAssociations = dto.UseRecordAssociations, CreatedBy = createdBy }; } diff --git a/CredentialManager/Services/IDataCouplerProfileService.cs b/CredentialManager/Services/IDataCouplerProfileService.cs index 1a11cd2..4917182 100644 --- a/CredentialManager/Services/IDataCouplerProfileService.cs +++ b/CredentialManager/Services/IDataCouplerProfileService.cs @@ -12,6 +12,11 @@ public interface IDataCouplerProfileService /// Task> GetAllProfilesAsync(); + /// + /// Ottiene tutti i profili per nome (inclusi quelli inattivi) + /// + Task GetProfileByNameIncludingInactiveAsync(string name); + /// /// Ottiene un profilo per ID /// diff --git a/CredentialManager/design_time_temp.db b/CredentialManager/design_time_temp.db index a1e3ded..88eb0a9 100644 Binary files a/CredentialManager/design_time_temp.db and b/CredentialManager/design_time_temp.db differ diff --git a/DataConnection/CredentialManagement/Interfaces/IDataConnectionCredentialService.cs b/DataConnection/CredentialManagement/Interfaces/IDataConnectionCredentialService.cs index 92f0fad..b2aea11 100644 --- a/DataConnection/CredentialManagement/Interfaces/IDataConnectionCredentialService.cs +++ b/DataConnection/CredentialManagement/Interfaces/IDataConnectionCredentialService.cs @@ -48,6 +48,9 @@ public interface IDataConnectionCredentialService Task GetRestServiceOptionsAsync(string credentialName); Task GetRestServiceOptionsAsync(int credentialId); + // Helper methods + Task GetCredentialIdByNameAsync(string name, CredentialManager.Models.CredentialType type); + // Connection testing Task<(bool Success, string Message)> TestDatabaseConnectionAsync(string credentialName); Task<(bool Success, string Message)> TestDatabaseConnectionAsync(DatabaseCredential credential); diff --git a/DataConnection/CredentialManagement/Services/DataConnectionCredentialService.cs b/DataConnection/CredentialManagement/Services/DataConnectionCredentialService.cs index 03eafcd..beb787b 100644 --- a/DataConnection/CredentialManagement/Services/DataConnectionCredentialService.cs +++ b/DataConnection/CredentialManagement/Services/DataConnectionCredentialService.cs @@ -936,5 +936,14 @@ public class DataConnectionCredentialService : IDataConnectionCredentialService return await _keyAssociationService.GetStatisticsAsync(); } + #region Helper Methods + + public async Task GetCredentialIdByNameAsync(string name, CredentialManager.Models.CredentialType type) + { + return await _credentialService.GetCredentialIdByNameAsync(name, type); + } + + #endregion + #endregion } diff --git a/Data_Coupler/Pages/DataCoupler.razor b/Data_Coupler/Pages/DataCoupler.razor index bee4946..1712ee9 100644 --- a/Data_Coupler/Pages/DataCoupler.razor +++ b/Data_Coupler/Pages/DataCoupler.razor @@ -997,6 +997,25 @@ Riepilogo Mapping } +
+ + +
+
@if (fieldMappings.Any()) { @@ -1124,23 +1143,6 @@ }
- -@if (isDatabaseConnected && isRestConnected && fieldMappings.Any()) -{ -
-
- -
-
-} - e.Name == profile.DestinationEndpoint); - if (entity != null) + var sourceCredential = await CredentialService.GetDatabaseCredentialAsync(profile.SourceCredentialId.Value); + if (sourceCredential != null) { - await SelectRestEntity(entity); + 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)) + { + Logger.LogInformation("Connessione con schema specifico: {Schema}", profile.SourceSchema); + await ConnectToDatabaseWithSchema(profile.SourceSchema); + } + else + { + Logger.LogInformation("Connessione senza schema specifico"); + await ConnectToDatabase(); + } + + 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}", + selectedTable, databaseTables.ContainsKey(profile.SourceTable)); + } + else + { + Logger.LogWarning("Impossibile selezionare tabella - Table: {Table}, Connected: {Connected}", + profile.SourceTable, isDatabaseConnected); + } } else { - Logger.LogWarning("Entità REST con endpoint {Endpoint} non trovata", profile.DestinationEndpoint); + Logger.LogWarning("Credenziale database con ID {CredentialId} non trovata", profile.SourceCredentialId); } } - } - } - - // Applica mapping dei campi se disponibile - if (!string.IsNullOrEmpty(profile.FieldMappingJson)) - { - try - { - var service = new DataCouplerProfileService(null!); // Temporaneo per deserializzazione - var mappings = service.DeserializeFieldMappings(profile.FieldMappingJson); - - // Applica i mapping - fieldMappings.Clear(); - keyFields.Clear(); - - foreach (var mapping in mappings) + else if (profile.SourceType == "file") { - fieldMappings[mapping.SourceField] = mapping.DestinationField; - if (mapping.IsKey) + // Per i file, non possiamo ricreare il file caricato, ma possiamo impostare le informazioni + if (!string.IsNullOrEmpty(profile.SourceFilePath)) { - keyFields.Add(mapping.DestinationField); + selectedFileName = Path.GetFileName(profile.SourceFilePath); + Logger.LogInformation("Informazioni file impostate: {FileName}", selectedFileName); } } - - Logger.LogInformation("Applicati {MappingCount} mapping dei campi dal profilo", mappings.Count); } - catch (Exception ex) + else { - Logger.LogWarning(ex, "Errore nel caricamento dei mapping dei campi dal profilo"); + Logger.LogInformation("Nessuna credenziale sorgente da configurare"); + } + + // Small delay to let source configuration settle + await Task.Delay(300); + + // Step 3: Configura e connetti la destinazione + if (profile.DestinationCredentialId.HasValue) + { + Logger.LogInformation("Step 3 - Configurazione destinazione con ID credenziale: {CredentialId}", profile.DestinationCredentialId); + + var destinationCredential = await CredentialService.GetRestApiCredentialAsync(profile.DestinationCredentialId.Value); + if (destinationCredential != null) + { + selectedRestCredential = destinationCredential.Name; + Logger.LogInformation("Credenziale REST selezionata: {Credential}", selectedRestCredential); + + // Force UI update for REST credential selection + StateHasChanged(); + await Task.Delay(200); + + // Connetti al servizio REST + Logger.LogInformation("Iniziando connessione REST..."); + await ConnectToRestApi(); + + Logger.LogInformation("Stato dopo connessione REST - Connected: {Connected}, Entities: {EntityCount}", + isRestConnected, restEntities.Count); + + // Seleziona l'entità REST se la connessione è riuscita + if (!string.IsNullOrEmpty(profile.DestinationEndpoint) && isRestConnected) + { + var entity = restEntities.FirstOrDefault(e => e.Name == profile.DestinationEndpoint); + if (entity != null) + { + Logger.LogInformation("Selezione entità REST: {Entity}", entity.Name); + await SelectRestEntity(entity); + Logger.LogInformation("Entità REST selezionata: {SelectedEntity}, Dettagli caricati: {DetailsLoaded}", + selectedRestEntity?.Name, restEntityDetails != null); + } + else + { + Logger.LogWarning("Entità REST non trovata: {Endpoint} - Entities disponibili: {Entities}", + profile.DestinationEndpoint, string.Join(", ", restEntities.Select(e => e.Name))); + } + } + else + { + Logger.LogWarning("Impossibile selezionare entità REST - Endpoint: {Endpoint}, Connected: {Connected}", + profile.DestinationEndpoint, isRestConnected); + } + } + else + { + Logger.LogWarning("Credenziale REST con ID {CredentialId} non trovata", profile.DestinationCredentialId); + } + } + else + { + Logger.LogInformation("Nessuna credenziale destinazione da configurare"); } - } - StateHasChanged(); + // Small delay to let destination configuration settle + await Task.Delay(300); + + // Step 4: Applica mapping dei campi se disponibile + if (!string.IsNullOrEmpty(profile.FieldMappingJson)) + { + Logger.LogInformation("Step 4 - Applicazione mapping campi..."); + try + { + var service = new DataCouplerProfileService(null!); + var mappings = service.DeserializeFieldMappings(profile.FieldMappingJson); + + Logger.LogInformation("Mappings deserializzati: {Count}", mappings.Count); + + // Applica i mapping + fieldMappings.Clear(); + keyFields.Clear(); + + foreach (var mapping in mappings) + { + fieldMappings[mapping.SourceField] = mapping.DestinationField; + if (mapping.IsKey) + { + keyFields.Add(mapping.DestinationField); + } + Logger.LogInformation("Mapping applicato: {Source} -> {Destination} (IsKey: {IsKey})", + mapping.SourceField, mapping.DestinationField, mapping.IsKey); + } + + Logger.LogInformation("Mappings applicati - Totale: {MappingCount}, Chiavi: {KeyCount}", + fieldMappings.Count, keyFields.Count); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Errore nel caricamento dei mapping dei campi dal profilo"); + } + } + else + { + Logger.LogInformation("Nessun mapping campi da applicare"); + } + + // Step 5: Applica configurazione chiave sorgente + if (!string.IsNullOrEmpty(profile.SourceKeyField)) + { + sourceKeyField = profile.SourceKeyField; + Logger.LogInformation("Step 5 - Chiave sorgente applicata: {SourceKey}", sourceKeyField); + } + else + { + Logger.LogInformation("Nessuna chiave sorgente da applicare"); + } + + // Step 6: Applica configurazione associazioni record + useRecordAssociations = profile.UseRecordAssociations; + Logger.LogInformation("Step 6 - Associazioni record configurate: {UseAssociations}", useRecordAssociations); + + Logger.LogInformation("=== FINE APPLICAZIONE PROFILO ==="); + Logger.LogInformation("Stato finale - Source: {SourceType}, DatabaseConnected: {DatabaseConnected}, RestConnected: {RestConnected}, Mappings: {MappingCount}", + selectedSourceType, isDatabaseConnected, isRestConnected, fieldMappings.Count); + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nell'applicazione della configurazione del profilo {ProfileName}", profile.Name); + await JSRuntime.InvokeVoidAsync("alert", $"Errore nel caricamento del profilo: {ex.Message}"); + } + finally + { + // Force final UI update + StateHasChanged(); + Logger.LogInformation("Aggiornamento finale UI completato"); + } } private async Task OnProfileSaved(DataCouplerProfileDto profileDto) { try { + Logger.LogInformation("Tentativo di salvataggio profilo: {ProfileName}", profileDto.Name); + var profileService = new DataCouplerProfileService(null!); // Usa il service di conversione var profile = profileService.FromDto(profileDto, "System"); // TODO: Usa utente corrente - await ProfileService.SaveProfileAsync(profile); - await LoadProfiles(); // Ricarica la lista + // 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); - await JSRuntime.InvokeVoidAsync("alert", $"Profilo '{profileDto.Name}' salvato con successo!"); + if (existingProfile != null) + { + Logger.LogInformation("Trovato profilo esistente con ID: {ProfileId}, IsActive: {IsActive}", + existingProfile.Id, existingProfile.IsActive); + + if (!existingProfile.IsActive) + { + // Il profilo esiste ma è inattivo - riattivalo e aggiornalo + Logger.LogInformation("Riattivazione del profilo inattivo: {ProfileName}", profileDto.Name); + profile.Id = existingProfile.Id; + profile.IsActive = true; + + // Aggiorna direttamente il profilo esistente invece di creare un nuovo oggetto + existingProfile.Description = profile.Description; + existingProfile.SourceType = profile.SourceType; + existingProfile.SourceCredentialId = profile.SourceCredentialId; + existingProfile.SourceSchema = profile.SourceSchema; + existingProfile.SourceTable = profile.SourceTable; + existingProfile.SourceFilePath = profile.SourceFilePath; + existingProfile.DestinationType = profile.DestinationType; + existingProfile.DestinationCredentialId = profile.DestinationCredentialId; + existingProfile.DestinationSchema = profile.DestinationSchema; + existingProfile.DestinationTable = profile.DestinationTable; + existingProfile.DestinationEndpoint = profile.DestinationEndpoint; + existingProfile.FieldMappingJson = profile.FieldMappingJson; + existingProfile.SourceKeyField = profile.SourceKeyField; + existingProfile.UseRecordAssociations = profile.UseRecordAssociations; + existingProfile.IsActive = true; + + await ProfileService.UpdateProfileAsync(existingProfile); + await LoadProfiles(); + + await JSRuntime.InvokeVoidAsync("alert", $"Profilo '{profileDto.Name}' riattivato e aggiornato con successo!"); + return; + } + + // Il profilo esiste ed è attivo - chiedi conferma per sovrascrittura + var confirmOverwrite = await JSRuntime.InvokeAsync("confirm", + $"Esiste già un profilo attivo con il nome '{profileDto.Name}'. Vuoi sovrascriverlo?"); + + if (confirmOverwrite) + { + Logger.LogInformation("Utente ha confermato la sovrascrittura del profilo: {ProfileName}", profileDto.Name); + + // Aggiorna il profilo esistente direttamente + existingProfile.Description = profile.Description; + existingProfile.SourceType = profile.SourceType; + existingProfile.SourceCredentialId = profile.SourceCredentialId; + existingProfile.SourceSchema = profile.SourceSchema; + existingProfile.SourceTable = profile.SourceTable; + existingProfile.SourceFilePath = profile.SourceFilePath; + existingProfile.DestinationType = profile.DestinationType; + existingProfile.DestinationCredentialId = profile.DestinationCredentialId; + existingProfile.DestinationSchema = profile.DestinationSchema; + existingProfile.DestinationTable = profile.DestinationTable; + existingProfile.DestinationEndpoint = profile.DestinationEndpoint; + existingProfile.FieldMappingJson = profile.FieldMappingJson; + existingProfile.SourceKeyField = profile.SourceKeyField; + existingProfile.UseRecordAssociations = profile.UseRecordAssociations; + + await ProfileService.UpdateProfileAsync(existingProfile); + await LoadProfiles(); // Ricarica la lista + + await JSRuntime.InvokeVoidAsync("alert", $"Profilo '{profileDto.Name}' aggiornato con successo!"); + } + else + { + Logger.LogInformation("Utente ha annullato la sovrascrittura del profilo: {ProfileName}", profileDto.Name); + + // Proponi di creare con un nome unico + var useUniqueName = await JSRuntime.InvokeAsync("confirm", + "Vuoi salvare il profilo con un nome unico automatico (es. 'Nome Profilo (1)')?"); + + if (useUniqueName) + { + var uniqueName = await GenerateUniqueProfileName(profileDto.Name); + profile.Name = uniqueName; + + try + { + await ProfileService.SaveProfileAsync(profile); + await LoadProfiles(); + + await JSRuntime.InvokeVoidAsync("alert", $"Profilo salvato con nome '{uniqueName}'!"); + } + catch (Exception uniqueEx) + { + Logger.LogError(uniqueEx, "Errore durante il salvataggio del profilo con nome unico: {UniqueName}", uniqueName); + + // Gestisci l'errore di unique constraint anche per il nome unico + if (uniqueEx.Message.Contains("UNIQUE constraint failed")) + { + await JSRuntime.InvokeVoidAsync("alert", + $"Errore: Non è stato possibile generare un nome unico per il profilo. " + + "Prova a ricaricare la pagina e riprova."); + } + else + { + await JSRuntime.InvokeVoidAsync("alert", $"Errore nel salvataggio del profilo: {uniqueEx.Message}"); + } + } + } + // Altrimenti, non salvare nulla + return; + } + } + else + { + Logger.LogInformation("Nessun profilo esistente trovato, creazione nuovo profilo: {ProfileName}", profileDto.Name); + + // Crea un nuovo profilo + try + { + await ProfileService.SaveProfileAsync(profile); + await LoadProfiles(); // Ricarica la lista + + await JSRuntime.InvokeVoidAsync("alert", $"Profilo '{profileDto.Name}' salvato con successo!"); + } + catch (Exception saveEx) + { + Logger.LogError(saveEx, "Errore durante il salvataggio del nuovo profilo: {ProfileName}", profileDto.Name); + + // Possibile race condition - riprova con controllo duplicato + if (saveEx.Message.Contains("UNIQUE constraint failed")) + { + Logger.LogWarning("Race condition rilevata durante il salvataggio, gestione del duplicato..."); + + // Chiedi se vuole sovrascrivere o creare nome unico + var handleDuplicate = await JSRuntime.InvokeAsync("confirm", + $"Un profilo con il nome '{profileDto.Name}' è stato creato nel frattempo. " + + "Vuoi sovrascriverlo? (Clicca 'Annulla' per salvare con un nome unico)"); + + if (handleDuplicate) + { + // Trova il profilo e aggiornalo + var duplicateProfile = await ProfileService.GetProfileByNameIncludingInactiveAsync(profileDto.Name); + if (duplicateProfile != null) + { + profile.Id = duplicateProfile.Id; + await ProfileService.UpdateProfileAsync(profile); + await LoadProfiles(); + + await JSRuntime.InvokeVoidAsync("alert", $"Profilo '{profileDto.Name}' aggiornato con successo!"); + } + else + { + await JSRuntime.InvokeVoidAsync("alert", "Errore: Il profilo duplicato non è stato trovato."); + } + } + else + { + // Crea con nome unico + var uniqueName = await GenerateUniqueProfileName(profileDto.Name); + profile.Name = uniqueName; + + await ProfileService.SaveProfileAsync(profile); + await LoadProfiles(); + + await JSRuntime.InvokeVoidAsync("alert", $"Profilo salvato con nome '{uniqueName}'!"); + } + } + else + { + throw; // Rilancia eccezioni non gestite + } + } + } } catch (Exception ex) { - Logger.LogError(ex, "Errore nel salvataggio del profilo"); - await JSRuntime.InvokeVoidAsync("alert", $"Errore nel salvataggio del profilo: {ex.Message}"); + Logger.LogError(ex, "Errore generale nel salvataggio del profilo: {ProfileName}", profileDto.Name); + + // Gestione generica degli errori + if (ex.Message.Contains("UNIQUE constraint failed")) + { + await JSRuntime.InvokeVoidAsync("alert", + $"Errore: Esiste già un profilo con il nome '{profileDto.Name}'. " + + "Questo può accadere se ci sono stati problemi di sincronizzazione. " + + "Prova a ricaricare la pagina e riprova."); + } + else + { + await JSRuntime.InvokeVoidAsync("alert", $"Errore nel salvataggio del profilo: {ex.Message}"); + } } } @@ -930,7 +1237,7 @@ public partial class DataCoupler : ComponentBase { isConnectingRest = false; } - } private async void SelectTable(string tableName) + } private async Task SelectTable(string tableName) { selectedTable = tableName; @@ -1005,7 +1312,9 @@ public partial class DataCoupler : ComponentBase } StateHasChanged(); - } private async Task SelectRestEntity(RestEntitySummary entity) + } + + private async Task SelectRestEntity(RestEntitySummary entity) { selectedRestEntity = entity; @@ -2015,6 +2324,7 @@ public partial class DataCoupler : ComponentBase /// /// Verifica se una query è una SELECT query sicura + /// private bool IsSelectQuery(string query) { @@ -2262,20 +2572,44 @@ public partial class DataCoupler : ComponentBase /// /// Ottiene l'ID della credenziale sorgente corrente /// - private int? GetCurrentSourceCredentialId() + private async Task GetCurrentSourceCredentialIdAsync() { - // TODO: Implementare logica per ottenere l'ID dalla credenziale selezionata - // Per ora ritorniamo null dato che i DTO non hanno ID + if (selectedSourceType == "database" && !string.IsNullOrEmpty(selectedDatabaseCredential)) + { + try + { + // Usa il nuovo metodo per ottenere direttamente l'ID della credenziale + return await CredentialService.GetCredentialIdByNameAsync(selectedDatabaseCredential, CredentialManager.Models.CredentialType.Database); + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nell'ottenere l'ID della credenziale database: {CredentialName}", selectedDatabaseCredential); + return null; + } + } + return null; } /// /// Ottiene l'ID della credenziale destinazione corrente /// - private int? GetCurrentDestinationCredentialId() + private async Task GetCurrentDestinationCredentialIdAsync() { - // TODO: Implementare logica per ottenere l'ID dalla credenziale selezionata - // Per ora ritorniamo null dato che i DTO non hanno ID + if (!string.IsNullOrEmpty(selectedRestCredential)) + { + try + { + // Usa il nuovo metodo per ottenere direttamente l'ID della credenziale + return await CredentialService.GetCredentialIdByNameAsync(selectedRestCredential, CredentialManager.Models.CredentialType.RestApi); + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nell'ottenere l'ID della credenziale REST: {CredentialName}", selectedRestCredential); + return null; + } + } + return null; } @@ -2400,7 +2734,7 @@ public partial class DataCoupler : ComponentBase catch (Exception ex) { Logger.LogError(ex, "Errore nella connessione con lo schema selezionato"); - databaseErrorMessage = $"Errore nella connessione con schema {selectedSchema}: {ex.Message}"; + databaseErrorMessage = $"Errore nella connessione al database {selectedSchema}: {ex.Message}"; } StateHasChanged(); @@ -2778,8 +3112,9 @@ public partial class DataCoupler : ComponentBase "id", "ID", "Id", "_id", "_ID", "_Id", "key", "KEY", "Key", - "code", "CODE", "Code", - "number", "NUMBER", "Number" + "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 @@ -2912,5 +3247,18 @@ public partial class DataCoupler : ComponentBase } } + private async Task GenerateUniqueProfileName(string baseName) + { + 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 new file mode 100644 index 0000000..e69de29 diff --git a/Data_Coupler/wwwroot/data/credentials.db b/Data_Coupler/wwwroot/data/credentials.db index 98fd888..cbd53c8 100644 Binary files a/Data_Coupler/wwwroot/data/credentials.db and b/Data_Coupler/wwwroot/data/credentials.db differ diff --git a/Data_Coupler/wwwroot/data/credentials.db-shm b/Data_Coupler/wwwroot/data/credentials.db-shm index 47db41f..a814b92 100644 Binary files a/Data_Coupler/wwwroot/data/credentials.db-shm and b/Data_Coupler/wwwroot/data/credentials.db-shm differ diff --git a/Data_Coupler/wwwroot/data/credentials.db-wal b/Data_Coupler/wwwroot/data/credentials.db-wal index 8e955bd..7c12581 100644 Binary files a/Data_Coupler/wwwroot/data/credentials.db-wal and b/Data_Coupler/wwwroot/data/credentials.db-wal differ