[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
+57
View File
@@ -350,4 +350,61 @@ public class OdbcDatabaseManager : IDatabaseManager
{
// Nessuna risorsa da rilasciare per ODBC diretto
}
/// <inheritdoc />
public async Task<bool> UpsertRecordAsync(string tableName, string keyField, object? keyValue, Dictionary<string, object?> record)
{
try
{
using var connection = new OdbcConnection(_connectionString);
await connection.OpenAsync();
// Controlla se il record esiste già (ODBC usa ? come placeholder)
using var checkCmd = new OdbcCommand($"SELECT COUNT(*) FROM {tableName} WHERE [{keyField}] = ?", connection);
checkCmd.Parameters.Add(new OdbcParameter { Value = keyValue ?? DBNull.Value });
var countResult = await checkCmd.ExecuteScalarAsync();
bool exists = Convert.ToInt64(countResult ?? 0L) > 0;
if (exists)
{
// UPDATE
var fields = record.Keys.ToList();
var setClauses = fields.Select(f => $"[{f}] = ?").ToList();
var updateSql = $"UPDATE {tableName} SET {string.Join(", ", setClauses)} WHERE [{keyField}] = ?";
using var updateCmd = new OdbcCommand(updateSql, connection);
foreach (var f in fields)
updateCmd.Parameters.Add(new OdbcParameter { Value = record[f] ?? DBNull.Value });
// Parametro per la WHERE
updateCmd.Parameters.Add(new OdbcParameter { Value = keyValue ?? DBNull.Value });
await updateCmd.ExecuteNonQueryAsync();
}
else
{
// INSERT
var fields = record.Keys.ToList();
var fieldNames = string.Join(", ", fields.Select(f => $"[{f}]"));
var paramPlaceholders = string.Join(", ", fields.Select(_ => "?"));
var insertSql = $"INSERT INTO {tableName} ({fieldNames}) VALUES ({paramPlaceholders})";
using var insertCmd = new OdbcCommand(insertSql, connection);
foreach (var f in fields)
insertCmd.Parameters.Add(new OdbcParameter { Value = record[f] ?? DBNull.Value });
await insertCmd.ExecuteNonQueryAsync();
}
return true;
}
catch (Exception ex)
{
Console.WriteLine($"Errore nell'upsert ODBC in {tableName}: {ex.Message}");
return false;
}
}
}