[Feature] Salesforce come fonte e Database come destinazione nel Data Coupler

Implementata la possibilita' di usare Salesforce come fonte dati e un database relazionale come destinazione nel flusso di trasferimento dati.

## Modifiche principali

### Nuovi file partial class
- Data_Coupler/Extensions/DataCoupler/SalesforceSourceMethod.cs
  - Stato e metodi per Salesforce come fonte (credenziali, connessione, discovery SObject)
  - ConnectToSalesforceSource(): autenticazione parallela + discovery summaries/details
  - SelectSalesforceSourceEntity(): selezione SObject e caricamento campi
  - GetAllRecordsFromSalesforceSource(): estrazione via ExtractAllEntitiesAsync con solo i campi mappati
  - Filtro/ricerca SObject in tempo reale

- Data_Coupler/Extensions/DataCoupler/DatabaseDestinationMethod.cs
  - Stato e metodi per database come destinazione
  - ConnectToDestinationDatabase(): connessione e discovery tabelle
  - SelectDestinationTable(): caricamento schema tabella on-demand
  - IsDestinationDatabaseReady: proprieta' calcolata per validazione
  - Toggle UI tra destinazione REST e Database

### IDatabaseManager interface
- Aggiunto UpsertRecordAsync(tableName, keyField, keyValue, record):
  - Esegue SELECT COUNT(*) per verificare esistenza record
  - UPDATE se esiste, INSERT se non esiste
  - Implementato in EFCoreDatabaseManager (parametri named @p0..@pN)
  - Implementato in OdbcDatabaseManager (parametri posizionali ?)

### DataCoupler.razor (UI)
- Aggiunto 'Salesforce (REST API)' nel dropdown tipo fonte
- Sezione UI Salesforce fonte: selettore credenziali, bottone connessione, lista SObject con ricerca
- Toggle destinazione REST/Database nella card destra
- Sezione UI Database destinazione: selettore credenziali, bottone connessione, lista tabelle con ricerca
- Colonna destra mapping aggiornata: mostra colonne DB se destinazione e' database, proprieta' REST altrimenti
- Colonna sinistra mapping: aggiunta sezione campi SObject Salesforce
- isSourceReady aggiornato per includere fonte Salesforce
- isDestinationReady aggiornato per includere destinazione database
- Etichette mapping dinamiche in base ai tipi selezionati

### DataCoupler.razor.cs (logica)
- LoadCredentials(): aggiunto caricamento credenziali Salesforce fonte
- ResetSourceState(): aggiunto reset stato Salesforce fonte
- ResetDestinationState(): aggiunto reset stato database destinazione
- GetAllRecordsFromSource(): aggiunto branch Salesforce
- StartDataTransfer(): routing verso StartDataTransferToDatabase() se dest=database
- Aggiunto StartDataTransferToDatabase(): estrae record fonte, applica mapping e default values, chiama UpsertRecordAsync per ogni record
- Rimosso codice duplicato in StartDataTransfer()
This commit is contained in:
2026-05-24 23:55:51 +02:00
parent b75e57fe31
commit 6452d45b77
8 changed files with 1091 additions and 41 deletions
+97 -10
View File
@@ -97,6 +97,7 @@ public partial class DataCoupler : ComponentBase
{
databaseCredentials = await CredentialService.GetAllDatabaseCredentialsAsync();
await LoadRestCredentials(); // Carica le credenziali REST dalla classe parziale
await LoadSalesforceSourceCredentials(); // Carica le credenziali Salesforce per la fonte
// Carica anche i profili disponibili
await LoadProfiles();
}
@@ -809,6 +810,7 @@ public partial class DataCoupler : ComponentBase
restSearchTerm = "";
currentRestDiscovery = null;
currentRestClient = null;
ResetDestinationDatabaseState();
}
private void OnSourceTypeChanged(ChangeEventArgs e)
@@ -833,6 +835,9 @@ public partial class DataCoupler : ComponentBase
fileData.Clear();
selectedSheet = "";
// Reset Salesforce source state
ResetSalesforceSourceState();
// Reset pagination
currentPage = 1;
@@ -1766,25 +1771,103 @@ public partial class DataCoupler : ComponentBase
}
private async Task StartDataTransfer()
{
// Verifica se possiamo utilizzare le chiamate Composite (solo per Salesforce)
// Se destinazione è database, usa il metodo dedicato
if (selectedDestinationType == "database")
{
await StartDataTransferToDatabase();
return;
}
// Verifica se possiamo utilizzare le chiamate Composite (solo per Salesforce REST dest)
if (currentRestClient is DataConnection.REST.Implementations.SalesforceServiceClient)
{
await StartDataTransferWithComposite();
return;
}
// Fallback al metodo originale per altri client REST
// Se siamo con Salesforce, usa il nuovo metodo Composite
if (currentRestClient is DataConnection.REST.Implementations.SalesforceServiceClient)
{
await StartDataTransferWithComposite();
return;
}
// Per altri client, usa il metodo originale
// Per altri client REST, usa il metodo originale
await StartDataTransferOriginal();
}
private async Task StartDataTransferToDatabase()
{
if (!fieldMappings.Any() || currentDestinationDatabaseManager == null || string.IsNullOrEmpty(selectedDestinationTable))
{
transferMessage = "Configurazione incompleta. Assicurati di aver selezionato la fonte, la tabella di destinazione e configurato almeno un mapping.";
transferMessageType = "error";
return;
}
if (string.IsNullOrEmpty(sourceKeyField))
{
transferMessage = "Seleziona un campo chiave sorgente per l'operazione di upsert.";
transferMessageType = "error";
return;
}
isTransferringData = true;
transferMessage = "";
int successCount = 0;
int errorCount = 0;
StateHasChanged();
try
{
var sourceRecords = await GetAllRecordsFromSource();
var recordsList = sourceRecords.ToList();
transferMessage = $"Elaborazione di {recordsList.Count} record...";
StateHasChanged();
foreach (var sourceRecord in recordsList)
{
try
{
// Applica mapping
var destRecord = new Dictionary<string, object?>();
foreach (var mapping in fieldMappings)
{
if (sourceRecord.TryGetValue(mapping.Key, out var value))
destRecord[mapping.Value] = value;
}
// Aggiungi default values
foreach (var dv in defaultValues)
{
if (!destRecord.ContainsKey(dv.Key))
destRecord[dv.Key] = dv.Value;
}
// Determina chiave di destinazione
string destKeyField = fieldMappings.TryGetValue(sourceKeyField, out var mapped) ? mapped : sourceKeyField;
object? keyValue = sourceRecord.TryGetValue(sourceKeyField, out var kv) ? kv : null;
var ok = await currentDestinationDatabaseManager.UpsertRecordAsync(
selectedDestinationTable, destKeyField, keyValue, destRecord);
if (ok) successCount++;
else errorCount++;
}
catch
{
errorCount++;
}
}
transferMessage = $"Trasferimento completato: {successCount} record elaborati, {errorCount} errori.";
transferMessageType = errorCount == 0 ? "success" : "warning";
}
catch (Exception ex)
{
transferMessage = $"Errore durante il trasferimento: {ex.Message}";
transferMessageType = "error";
Logger.LogError(ex, "Errore nel trasferimento verso database");
}
finally
{
isTransferringData = false;
StateHasChanged();
}
}
private async Task StartDataTransferOriginal()
{
if (!fieldMappings.Any() || currentRestClient == null || selectedRestEntity == null)
@@ -2248,6 +2331,10 @@ public partial class DataCoupler : ComponentBase
{
return await GetAllRecordsFromFile();
}
else if (selectedSourceType == "salesforce")
{
return await GetAllRecordsFromSalesforceSource();
}
return new List<Dictionary<string, object>>();
}