diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 4a46459..e0b2f96 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -145,6 +145,7 @@ - **Storico Esecuzioni**: Log completo con timestamp, record processati, errori - **Pausa/Riprendi**: Controllo dinamico schedulazioni - **Override Database**: Possibilità di sovrascrivere sorgente/destinazione +- **Deletion Sync Configurabile**: Opzione per abilitare sincronizzazione eliminazioni (disabilitata di default) #### File Chiave: - `CredentialManager/Models/ProfileSchedule.cs` @@ -191,9 +192,23 @@ - **Gestione Associazioni**: Aggiorna/elimina associazioni correlate - **Modalità Sicura**: Preview eliminazioni prima dell'esecuzione - **Logging Completo**: Traccia tutte le operazioni di eliminazione +- **Configurazione Granulare**: + - **Disabilitata** completamente nei trasferimenti manuali (DataCoupler.razor) + - **Configurabile** nelle schedulazioni tramite flag `EnableDeletionSync` + - **Default: false** per massima sicurezza + - Warning esplicito nell'UI per operazioni critiche + +#### Sicurezza: +- La funzionalità è **disabilitata di default** per evitare eliminazioni accidentali +- Disponibile **solo per le schedulazioni** con configurazione esplicita +- L'utente deve attivamente abilitare la funzione con piena consapevolezza +- Logging completo di tutte le operazioni di eliminazione per audit trail #### File Chiave: - `Data_Coupler/Services/DeletionSyncService.cs` +- `Data_Coupler/Services/ScheduledProfileExecutionService.cs` +- `CredentialManager/Models/ProfileSchedule.cs` (campo `EnableDeletionSync`) +- `Data_Coupler/Pages/Scheduling.razor` (UI configurazione) ### 8. Sistema di Autenticazione diff --git a/CredentialManager/Migrations/20260123104841_AddEnableDeletionSyncToProfileSchedule.Designer.cs b/CredentialManager/Migrations/20260123104841_AddEnableDeletionSyncToProfileSchedule.Designer.cs new file mode 100644 index 0000000..897a8d6 --- /dev/null +++ b/CredentialManager/Migrations/20260123104841_AddEnableDeletionSyncToProfileSchedule.Designer.cs @@ -0,0 +1,585 @@ +// +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("20260123104841_AddEnableDeletionSyncToProfileSchedule")] + partial class AddEnableDeletionSyncToProfileSchedule + { + /// + 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("DeletionAction") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("DeletionMarkField") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DeletionMarkValue") + .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("SourceCustomQuery") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + 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("SyncDeletions") + .HasColumnType("INTEGER"); + + 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("Data_Hash") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("DeletionSynced") + .HasColumnType("INTEGER"); + + b.Property("DeletionSyncedAt") + .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("IsSourceDeleted") + .HasColumnType("INTEGER"); + + b.Property("KeyValue") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("LastVerifiedAt") + .HasColumnType("TEXT"); + + b.Property("MappedDestinationField") + .HasMaxLength(200) + .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.ProfileSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DailyTime") + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("DayOfMonth") + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DestinationDatabaseOverride") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("EnableDeletionSync") + .HasColumnType("INTEGER"); + + b.Property("ExecutionCount") + .HasColumnType("INTEGER"); + + b.Property("IntervalUnit") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("IntervalValue") + .HasColumnType("INTEGER"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("LastExecutionMessage") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("LastExecutionRecordCount") + .HasColumnType("INTEGER"); + + b.Property("LastExecutionStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("LastExecutionTime") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("NextExecutionTime") + .HasColumnType("TEXT"); + + b.Property("ProfileId") + .HasColumnType("INTEGER"); + + b.Property("ScheduleType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("ScheduledDateTime") + .HasColumnType("TEXT"); + + b.Property("SourceDatabaseOverride") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProfileId"); + + b.ToTable("ProfileSchedules"); + }); + + modelBuilder.Entity("CredentialManager.Models.ScheduleExecutionHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdditionalInfo") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DestinationInfo") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DestinationType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("EndTime") + .HasColumnType("TEXT"); + + b.Property("ErrorDetails") + .HasMaxLength(5000) + .HasColumnType("TEXT"); + + b.Property("Message") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("ProfileId") + .HasColumnType("INTEGER"); + + b.Property("ProfileName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RecordsProcessed") + .HasColumnType("INTEGER"); + + b.Property("RecordsWithErrors") + .HasColumnType("INTEGER"); + + b.Property("ScheduleId") + .HasColumnType("INTEGER"); + + b.Property("SourceInfo") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("SourceType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("TriggerType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("TriggeredBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProfileId"); + + b.HasIndex("ScheduleId"); + + b.HasIndex("StartTime"); + + b.HasIndex("Status"); + + b.HasIndex("TriggerType"); + + b.ToTable("ScheduleExecutionHistories", (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"); + }); + + modelBuilder.Entity("CredentialManager.Models.ProfileSchedule", b => + { + b.HasOne("CredentialManager.Models.DataCouplerProfile", "Profile") + .WithMany() + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("CredentialManager.Models.ScheduleExecutionHistory", b => + { + b.HasOne("CredentialManager.Models.ProfileSchedule", "Schedule") + .WithMany() + .HasForeignKey("ScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Schedule"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CredentialManager/Migrations/20260123104841_AddEnableDeletionSyncToProfileSchedule.cs b/CredentialManager/Migrations/20260123104841_AddEnableDeletionSyncToProfileSchedule.cs new file mode 100644 index 0000000..d202b37 --- /dev/null +++ b/CredentialManager/Migrations/20260123104841_AddEnableDeletionSyncToProfileSchedule.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CredentialManager.Migrations +{ + /// + public partial class AddEnableDeletionSyncToProfileSchedule : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "EnableDeletionSync", + table: "ProfileSchedules", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "EnableDeletionSync", + table: "ProfileSchedules"); + } + } +} diff --git a/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs b/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs index 885c024..64a742e 100644 --- a/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs +++ b/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs @@ -382,6 +382,9 @@ namespace CredentialManager.Migrations .HasMaxLength(100) .HasColumnType("TEXT"); + b.Property("EnableDeletionSync") + .HasColumnType("INTEGER"); + b.Property("ExecutionCount") .HasColumnType("INTEGER"); diff --git a/CredentialManager/Models/ProfileSchedule.cs b/CredentialManager/Models/ProfileSchedule.cs index 9de9e3c..593931f 100644 --- a/CredentialManager/Models/ProfileSchedule.cs +++ b/CredentialManager/Models/ProfileSchedule.cs @@ -70,6 +70,9 @@ public class ProfileSchedule [MaxLength(100)] public string? DestinationDatabaseOverride { get; set; } + // Configurazione sincronizzazione eliminazioni (default: disabilitata) + public bool EnableDeletionSync { get; set; } = false; + // Metadati [MaxLength(100)] public string? CreatedBy { get; set; } diff --git a/CredentialManager/design_time_temp.db b/CredentialManager/design_time_temp.db index 589aa35..0482876 100644 Binary files a/CredentialManager/design_time_temp.db and b/CredentialManager/design_time_temp.db differ diff --git a/Data_Coupler/Models/BackupModels.cs b/Data_Coupler/Models/BackupModels.cs index 4eeb651..423ef01 100644 --- a/Data_Coupler/Models/BackupModels.cs +++ b/Data_Coupler/Models/BackupModels.cs @@ -289,6 +289,9 @@ public class ProfileScheduleBackup [JsonPropertyName("createdBy")] public string? CreatedBy { get; set; } + + [JsonPropertyName("enableDeletionSync")] + public bool EnableDeletionSync { get; set; } = false; } /// diff --git a/Data_Coupler/Pages/DataCoupler.razor.cs b/Data_Coupler/Pages/DataCoupler.razor.cs index 6cf9b76..ff532e7 100644 --- a/Data_Coupler/Pages/DataCoupler.razor.cs +++ b/Data_Coupler/Pages/DataCoupler.razor.cs @@ -1496,8 +1496,10 @@ public partial class DataCoupler : ComponentBase recordNumber++; } - // 3.5 Sincronizza le cancellazioni (se abilitato) + // 3.5 Sincronizzazione cancellazioni (DISABILITATA per trasferimenti manuali) + // Questa funzionalità è disponibile solo per le schedulazioni con configurazione esplicita int deletedCount = 0; + /* DELETION SYNC DISABILITATA PER TRASFERIMENTI MANUALI if (useRecordAssociations && !string.IsNullOrEmpty(sourceKeyField)) { try @@ -1570,6 +1572,7 @@ public partial class DataCoupler : ComponentBase }); } } + */ // 4. Mostra risultati if (errorCount == 0) diff --git a/Data_Coupler/Pages/Scheduling.razor b/Data_Coupler/Pages/Scheduling.razor index d780b72..d6bd0b3 100644 --- a/Data_Coupler/Pages/Scheduling.razor +++ b/Data_Coupler/Pages/Scheduling.razor @@ -336,6 +336,30 @@ +
+
+
+ Opzioni Avanzate +
+
+
+
+ + +
+
+ + + Attenzione: Se abilitata, i record eliminati dalla sorgente saranno automaticamente eliminati anche dalla destinazione durante l'esecuzione schedulata. + Questa opzione è disabilitata di default per motivi di sicurezza. + Usare con cautela! + +
+
+
+
Task ExecuteProfileAsync(int profileId); + + /// + /// Esegue un profilo Data Coupler specificato dall'ID con configurazione sincronizzazione eliminazioni + /// + /// ID del profilo da eseguire + /// Se true, sincronizza le eliminazioni dalla sorgente alla destinazione + Task ExecuteProfileAsync(int profileId, bool enableDeletionSync); } \ No newline at end of file diff --git a/Data_Coupler/Services/ScheduledExecutionBackgroundService.cs b/Data_Coupler/Services/ScheduledExecutionBackgroundService.cs index d0cfbc0..1b5ccaf 100644 --- a/Data_Coupler/Services/ScheduledExecutionBackgroundService.cs +++ b/Data_Coupler/Services/ScheduledExecutionBackgroundService.cs @@ -69,11 +69,11 @@ public class ScheduledExecutionBackgroundService : BackgroundService { if (ShouldExecuteSchedule(schedule, currentTime)) { - _logger.LogInformation("Esecuzione schedulata per profilo: {ProfileName} (Schedule: {ScheduleName})", - schedule.Profile?.Name ?? "N/A", schedule.Name); + _logger.LogInformation("Esecuzione schedulata per profilo: {ProfileName} (Schedule: {ScheduleName}) - DeletionSync: {DeletionSync}", + schedule.Profile?.Name ?? "N/A", schedule.Name, schedule.EnableDeletionSync); - // Esegui il profilo - var result = await executionService.ExecuteProfileAsync(schedule.ProfileId); + // Esegui il profilo con il flag deletion sync dalla schedulazione + var result = await executionService.ExecuteProfileAsync(schedule.ProfileId, schedule.EnableDeletionSync); // Aggiorna la schedulazione await UpdateScheduleAfterExecution(scheduleService, schedule, currentTime, result.Success); diff --git a/Data_Coupler/Services/ScheduledProfileExecutionService.cs b/Data_Coupler/Services/ScheduledProfileExecutionService.cs index 2d51457..2413df4 100644 --- a/Data_Coupler/Services/ScheduledProfileExecutionService.cs +++ b/Data_Coupler/Services/ScheduledProfileExecutionService.cs @@ -28,6 +28,7 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic private readonly IDataConnectionCredentialService _dataConnectionCredentialService; private readonly IKeyAssociationService _keyAssociationService; private readonly IAssociationService _associationService; + private readonly IDeletionSyncService _deletionSyncService; private readonly ILogger _logger; public ScheduledProfileExecutionService( @@ -37,6 +38,7 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic IDataConnectionCredentialService dataConnectionCredentialService, IKeyAssociationService keyAssociationService, IAssociationService associationService, + IDeletionSyncService deletionSyncService, ILogger logger) { _profileService = profileService; @@ -45,6 +47,7 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic _dataConnectionCredentialService = dataConnectionCredentialService; _keyAssociationService = keyAssociationService; _associationService = associationService; + _deletionSyncService = deletionSyncService; _logger = logger; } @@ -52,6 +55,14 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic /// Esegue un profilo Data Coupler specificato dall'ID /// public async Task ExecuteProfileAsync(int profileId) + { + return await ExecuteProfileAsync(profileId, enableDeletionSync: false); + } + + /// + /// Esegue un profilo Data Coupler specificato dall'ID con configurazione sincronizzazione eliminazioni + /// + public async Task ExecuteProfileAsync(int profileId, bool enableDeletionSync) { var startTime = DateTime.UtcNow; var result = new ProfileExecutionResult @@ -61,7 +72,7 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic try { - _logger.LogInformation("Inizio esecuzione profilo schedulato ID: {ProfileId}", profileId); + _logger.LogInformation("Inizio esecuzione profilo schedulato ID: {ProfileId} - DeletionSync: {DeletionSync}", profileId, enableDeletionSync); // Carica il profilo var profile = await _profileService.GetProfileByIdAsync(profileId); @@ -78,7 +89,7 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic await _profileService.UpdateLastUsedAsync(profile.Id); // Esegue il trasferimento dati con la logica completa - var recordsTransferred = await ExecuteDataTransferAsync(profile); + var recordsTransferred = await ExecuteDataTransferAsync(profile, enableDeletionSync); result.Success = true; result.RecordsProcessed = recordsTransferred; @@ -106,10 +117,11 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic /// Metodo principale per l'esecuzione del trasferimento dati /// Implementa la stessa logica di StartDataTransferWithComposite /// - private async Task ExecuteDataTransferAsync(DataCouplerProfile profile) + private async Task ExecuteDataTransferAsync(DataCouplerProfile profile, bool enableDeletionSync = false) { _logger.LogInformation("=== INIZIO TRASFERIMENTO DATI SCHEDULATO ==="); - _logger.LogInformation("Esecuzione profilo: {ProfileName} (ID: {ProfileId})", profile.Name, profile.Id); + _logger.LogInformation("Esecuzione profilo: {ProfileName} (ID: {ProfileId}) - DeletionSync: {DeletionSync}", + profile.Name, profile.Id, enableDeletionSync); try { @@ -151,12 +163,12 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic if (useSalesforceComposite) { _logger.LogInformation("Utilizzo Salesforce Composite API per il trasferimento"); - return await ExecuteDataTransferWithCompositeAsync(profile, sourceRecords, restClient, restEntity, restCredential!, fieldMappings); + return await ExecuteDataTransferWithCompositeAsync(profile, sourceRecords, restClient, restEntity, restCredential!, fieldMappings, enableDeletionSync); } else { _logger.LogInformation("Utilizzo metodo trasferimento standard per il trasferimento"); - return await ExecuteDataTransferStandardAsync(profile, sourceRecords, restClient, restEntity, restCredential!, fieldMappings); + return await ExecuteDataTransferStandardAsync(profile, sourceRecords, restClient, restEntity, restCredential!, fieldMappings, enableDeletionSync); } } catch (Exception ex) @@ -389,9 +401,11 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic IRestServiceClient restClient, RestEntitySummary restEntity, RestApiCredential restCredential, - Dictionary fieldMappings) + Dictionary fieldMappings, + bool enableDeletionSync = false) { - _logger.LogInformation("Iniziando trasferimento dati standard per {RecordCount} record", sourceRecords.Count()); + _logger.LogInformation("Iniziando trasferimento dati standard per {RecordCount} record - DeletionSync: {DeletionSync}", + sourceRecords.Count(), enableDeletionSync); int successCount = 0; int errorCount = 0; @@ -454,6 +468,50 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic _logger.LogInformation("Trasferimento completato. Successi: {SuccessCount}, Errori: {ErrorCount}", successCount, errorCount); + // Sincronizzazione cancellazioni (se abilitata) + if (enableDeletionSync && profile.UseRecordAssociations && !string.IsNullOrEmpty(profile.SourceKeyField)) + { + try + { + _logger.LogInformation("SCHEDULED: Inizio sincronizzazione cancellazioni..."); + + // Estrai tutti i valori chiave presenti nella sorgente + var sourceKeyValues = sourceRecords + .Select(r => r.ContainsKey(profile.SourceKeyField) ? r[profile.SourceKeyField]?.ToString() : null) + .Where(k => !string.IsNullOrEmpty(k)) + .Cast() + .Distinct() + .ToList(); + + _logger.LogInformation("SCHEDULED: Trovati {Count} valori chiave nella sorgente", sourceKeyValues.Count); + + // Sincronizza le cancellazioni + var deletionOptions = new DeletionSyncOptions + { + Action = DeletionAction.Delete // Default: elimina fisicamente + }; + + var deletionResult = await _deletionSyncService.SyncDeletionsAsync( + sourceKeyValues, + restEntity.Name, + restCredential.Name, + restClient, + deletionOptions); + + if (deletionResult.DeletedRecordsDetected > 0) + { + _logger.LogInformation("SCHEDULED: Sincronizzazione cancellazioni completata - {Detected} rilevati, {Synced} sincronizzati, {Errors} errori", + deletionResult.DeletedRecordsDetected, + deletionResult.DeletedRecordsSynced, + deletionResult.SyncErrors); + } + } + catch (Exception delEx) + { + _logger.LogError(delEx, "SCHEDULED: Errore durante la sincronizzazione delle cancellazioni"); + } + } + return successCount; } @@ -467,15 +525,17 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic IRestServiceClient restClient, RestEntitySummary restEntity, RestApiCredential restCredential, - Dictionary fieldMappings) + Dictionary fieldMappings, + bool enableDeletionSync = false) { - _logger.LogInformation("Iniziando trasferimento dati COMPOSITE per {RecordCount} record", sourceRecords.Count()); + _logger.LogInformation("Iniziando trasferimento dati COMPOSITE per {RecordCount} record - DeletionSync: {DeletionSync}", + sourceRecords.Count(), enableDeletionSync); // Verifica che sia effettivamente un SalesforceServiceClient if (!(restClient is DataConnection.REST.Implementations.SalesforceServiceClient salesforceClient)) { _logger.LogWarning("Client REST non è SalesforceServiceClient, fallback al metodo standard"); - return await ExecuteDataTransferStandardAsync(profile, sourceRecords, restClient, restEntity, restCredential, fieldMappings); + return await ExecuteDataTransferStandardAsync(profile, sourceRecords, restClient, restEntity, restCredential, fieldMappings, enableDeletionSync); } try @@ -738,6 +798,50 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic _logger.LogInformation("COMPOSITE SCHEDULED: Trasferimento completato. Creazioni: {SuccessCount}, Aggiornamenti: {UpdatedCount}, Saltati: {SkippedCount}, Errori: {ErrorCount}", successCount, updatedCount, skippedCount, errorCount); + // Sincronizzazione cancellazioni (se abilitata) + if (enableDeletionSync && currentUseRecordAssociations && !string.IsNullOrEmpty(profile.SourceKeyField)) + { + try + { + _logger.LogInformation("COMPOSITE SCHEDULED: Inizio sincronizzazione cancellazioni..."); + + // Estrai tutti i valori chiave presenti nella sorgente + var sourceKeyValues = sourceRecords + .Select(r => r.ContainsKey(profile.SourceKeyField) ? r[profile.SourceKeyField]?.ToString() : null) + .Where(k => !string.IsNullOrEmpty(k)) + .Cast() + .Distinct() + .ToList(); + + _logger.LogInformation("COMPOSITE SCHEDULED: Trovati {Count} valori chiave nella sorgente", sourceKeyValues.Count); + + // Sincronizza le cancellazioni + var deletionOptions = new DeletionSyncOptions + { + Action = DeletionAction.Delete // Default: elimina fisicamente + }; + + var deletionResult = await _deletionSyncService.SyncDeletionsAsync( + sourceKeyValues, + currentEntityName, + currentCredentialName, + restClient, + deletionOptions); + + if (deletionResult.DeletedRecordsDetected > 0) + { + _logger.LogInformation("COMPOSITE SCHEDULED: Sincronizzazione cancellazioni completata - {Detected} rilevati, {Synced} sincronizzati, {Errors} errori", + deletionResult.DeletedRecordsDetected, + deletionResult.DeletedRecordsSynced, + deletionResult.SyncErrors); + } + } + catch (Exception delEx) + { + _logger.LogError(delEx, "COMPOSITE SCHEDULED: Errore durante la sincronizzazione delle cancellazioni"); + } + } + return totalProcessed; } catch (Exception ex) diff --git a/README.md b/README.md index 0e53693..6a8807b 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,10 @@ docker build -t data-coupler:local-windows -f Dockerfile.windows . - **Validazione**: Validazione completa dei dati in input - **Isolamento**: Ogni progetto ha responsabilità specifiche - **Type Safety**: Uso di tipi forti per evitare errori +- **Deletion Sync Sicuro**: + - Disabilitato di default per prevenire eliminazioni accidentali + - Disponibile solo nelle schedulazioni con configurazione esplicita + - Warning chiaro nell'interfaccia utente per operazioni critiche ## Testing