[Feature] Disabilitata deletion sync nei trasferimenti manuali e aggiunta configurazione nelle schedulazioni
- Disabilitata completamente la sincronizzazione eliminazioni nei trasferimenti manuali (DataCoupler.razor.cs) - Aggiunto campo EnableDeletionSync al modello ProfileSchedule (default: false) - Implementata logica condizionale in ScheduledProfileExecutionService per deletion sync - Aggiunta sezione 'Opzioni Avanzate' nell'interfaccia schedulazione con warning - Creata migration Entity Framework AddEnableDeletionSyncToProfileSchedule - Aggiornato BackupModels per supporto backup/restore del nuovo campo - Aggiornata documentazione README.md e copilot-instructions.md - La deletion sync è ora disponibile solo per schedulazioni con configurazione esplicita per massima sicurezza
This commit is contained in:
@@ -289,6 +289,9 @@ public class ProfileScheduleBackup
|
||||
|
||||
[JsonPropertyName("createdBy")]
|
||||
public string? CreatedBy { get; set; }
|
||||
|
||||
[JsonPropertyName("enableDeletionSync")]
|
||||
public bool EnableDeletionSync { get; set; } = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -336,6 +336,30 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-warning text-dark">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-exclamation-triangle"></i> Opzioni Avanzate
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-check mb-2">
|
||||
<InputCheckbox @bind-Value="editingSchedule.EnableDeletionSync" class="form-check-input" id="enableDeletionSyncCheckbox" />
|
||||
<label class="form-check-label" for="enableDeletionSyncCheckbox">
|
||||
<strong>Abilita sincronizzazione eliminazioni</strong>
|
||||
</label>
|
||||
</div>
|
||||
<div class="alert alert-warning mb-0">
|
||||
<small>
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<strong>Attenzione:</strong> Se abilitata, i record eliminati dalla sorgente saranno automaticamente eliminati anche dalla destinazione durante l'esecuzione schedulata.
|
||||
Questa opzione è <strong>disabilitata di default</strong> per motivi di sicurezza.
|
||||
Usare con cautela!
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annulla</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
|
||||
@@ -21,4 +21,11 @@ public interface IScheduledProfileExecutionService
|
||||
/// Esegue un profilo Data Coupler specificato dall'ID
|
||||
/// </summary>
|
||||
Task<ProfileExecutionResult> ExecuteProfileAsync(int profileId);
|
||||
|
||||
/// <summary>
|
||||
/// Esegue un profilo Data Coupler specificato dall'ID con configurazione sincronizzazione eliminazioni
|
||||
/// </summary>
|
||||
/// <param name="profileId">ID del profilo da eseguire</param>
|
||||
/// <param name="enableDeletionSync">Se true, sincronizza le eliminazioni dalla sorgente alla destinazione</param>
|
||||
Task<ProfileExecutionResult> ExecuteProfileAsync(int profileId, bool enableDeletionSync);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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<ScheduledProfileExecutionService> _logger;
|
||||
|
||||
public ScheduledProfileExecutionService(
|
||||
@@ -37,6 +38,7 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
|
||||
IDataConnectionCredentialService dataConnectionCredentialService,
|
||||
IKeyAssociationService keyAssociationService,
|
||||
IAssociationService associationService,
|
||||
IDeletionSyncService deletionSyncService,
|
||||
ILogger<ScheduledProfileExecutionService> 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
|
||||
/// </summary>
|
||||
public async Task<ProfileExecutionResult> ExecuteProfileAsync(int profileId)
|
||||
{
|
||||
return await ExecuteProfileAsync(profileId, enableDeletionSync: false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Esegue un profilo Data Coupler specificato dall'ID con configurazione sincronizzazione eliminazioni
|
||||
/// </summary>
|
||||
public async Task<ProfileExecutionResult> 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
|
||||
/// </summary>
|
||||
private async Task<int> ExecuteDataTransferAsync(DataCouplerProfile profile)
|
||||
private async Task<int> 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<string, string> fieldMappings)
|
||||
Dictionary<string, string> 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<string>()
|
||||
.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<string, string> fieldMappings)
|
||||
Dictionary<string, string> 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<string>()
|
||||
.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)
|
||||
|
||||
Reference in New Issue
Block a user