[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:
@@ -0,0 +1,299 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using CredentialManager.Models;
|
||||
using DataConnection.REST.Interfaces;
|
||||
using DataConnection.REST.Models;
|
||||
using Data_Coupler.Services;
|
||||
|
||||
namespace Data_Coupler.Pages;
|
||||
|
||||
/// <summary>
|
||||
/// Partial class per la gestione di Salesforce come sorgente dati.
|
||||
/// Consente di autenticarsi a Salesforce, scoprire gli SObject disponibili
|
||||
/// e selezionare un'entità da cui estrarre i dati da trasferire.
|
||||
/// </summary>
|
||||
public partial class DataCoupler : ComponentBase
|
||||
{
|
||||
// ===== PROPRIETÀ SALESFORCE SOURCE =====
|
||||
|
||||
/// <summary>Credenziali Salesforce disponibili come sorgente</summary>
|
||||
protected List<RestApiCredential> salesforceSourceCredentials = new();
|
||||
|
||||
/// <summary>Credenziale Salesforce selezionata come sorgente</summary>
|
||||
protected string selectedSalesforceSourceCredential = "";
|
||||
|
||||
/// <summary>Stato connessione in corso</summary>
|
||||
protected bool isConnectingSalesforceSource = false;
|
||||
|
||||
/// <summary>Salesforce source connessa con successo</summary>
|
||||
protected bool isSalesforceSourceConnected = false;
|
||||
|
||||
/// <summary>Messaggio di errore connessione Salesforce source</summary>
|
||||
protected string salesforceSourceErrorMessage = "";
|
||||
|
||||
/// <summary>Lista degli SObject Salesforce disponibili (summaries)</summary>
|
||||
protected List<RestEntitySummary> salesforceSourceEntities = new();
|
||||
|
||||
/// <summary>SObject Salesforce selezionato come sorgente</summary>
|
||||
protected RestEntitySummary? selectedSalesforceSourceEntity = null;
|
||||
|
||||
/// <summary>Dettagli (campi) dell'SObject selezionato</summary>
|
||||
protected RestEntityInfo? salesforceSourceEntityDetails = null;
|
||||
|
||||
/// <summary>Termine di ricerca per filtrare gli SObject</summary>
|
||||
protected string salesforceSourceSearchTerm = "";
|
||||
|
||||
/// <summary>Client REST per le operazioni Salesforce source</summary>
|
||||
protected IRestServiceClient? currentSalesforceSourceClient = null;
|
||||
|
||||
/// <summary>Discovery metadata per Salesforce source</summary>
|
||||
protected IRestMetadataDiscovery? currentSalesforceSourceDiscovery = null;
|
||||
|
||||
// ===== METODI SALESFORCE SOURCE =====
|
||||
|
||||
/// <summary>
|
||||
/// Carica le credenziali di tipo Salesforce per usarle come sorgente
|
||||
/// </summary>
|
||||
protected async Task LoadSalesforceSourceCredentials()
|
||||
{
|
||||
try
|
||||
{
|
||||
var allCreds = await CredentialService.GetAllRestApiCredentialsAsync();
|
||||
salesforceSourceCredentials = allCreds
|
||||
.Where(c => c.ServiceType == RestServiceType.Salesforce)
|
||||
.ToList();
|
||||
Logger.LogInformation("Caricate {Count} credenziali Salesforce per uso come sorgente", salesforceSourceCredentials.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Errore nel caricamento delle credenziali Salesforce source");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gestisce il cambio di credenziale Salesforce source
|
||||
/// </summary>
|
||||
protected void OnSalesforceSourceCredentialChanged(ChangeEventArgs e)
|
||||
{
|
||||
var newCredential = e.Value?.ToString() ?? "";
|
||||
|
||||
// Pulisce la cache se si cambia credenziale
|
||||
if (!string.IsNullOrEmpty(selectedSalesforceSourceCredential) && selectedSalesforceSourceCredential != newCredential)
|
||||
{
|
||||
try { ConnectionFactory.ClearRestClientCache(selectedSalesforceSourceCredential); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
selectedSalesforceSourceCredential = newCredential;
|
||||
ResetSalesforceSourceState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resetta lo stato della connessione Salesforce source
|
||||
/// </summary>
|
||||
protected void ResetSalesforceSourceState()
|
||||
{
|
||||
isSalesforceSourceConnected = false;
|
||||
salesforceSourceEntities.Clear();
|
||||
selectedSalesforceSourceEntity = null;
|
||||
salesforceSourceEntityDetails = null;
|
||||
salesforceSourceSearchTerm = "";
|
||||
salesforceSourceErrorMessage = "";
|
||||
currentSalesforceSourceDiscovery = null;
|
||||
currentSalesforceSourceClient = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Si connette a Salesforce come sorgente e scopre gli SObject disponibili
|
||||
/// </summary>
|
||||
protected async Task ConnectToSalesforceSource()
|
||||
{
|
||||
if (string.IsNullOrEmpty(selectedSalesforceSourceCredential))
|
||||
return;
|
||||
|
||||
isConnectingSalesforceSource = true;
|
||||
salesforceSourceErrorMessage = "";
|
||||
|
||||
try
|
||||
{
|
||||
// Verifica la credenziale
|
||||
var credential = salesforceSourceCredentials.FirstOrDefault(c => c.Name == selectedSalesforceSourceCredential);
|
||||
if (credential == null)
|
||||
{
|
||||
salesforceSourceErrorMessage = "Credenziale Salesforce non trovata";
|
||||
return;
|
||||
}
|
||||
|
||||
// Crea i client usando il factory
|
||||
currentSalesforceSourceClient = await ConnectionFactory.CreateRestServiceClientAsync(selectedSalesforceSourceCredential);
|
||||
currentSalesforceSourceDiscovery = await ConnectionFactory.CreateRestMetadataDiscoveryAsync(selectedSalesforceSourceCredential);
|
||||
|
||||
// Autenticazione
|
||||
Logger.LogInformation("Avvio autenticazione Salesforce source: {Credential}", selectedSalesforceSourceCredential);
|
||||
var authResult = await currentSalesforceSourceClient.AuthenticateAsync();
|
||||
if (!authResult)
|
||||
{
|
||||
salesforceSourceErrorMessage = "Autenticazione Salesforce fallita. Verificare le credenziali.";
|
||||
currentSalesforceSourceClient = null;
|
||||
currentSalesforceSourceDiscovery = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Discovery parallela: summaries veloci → UI interattiva subito; dettagli completi in background
|
||||
Logger.LogInformation("Avvio discovery parallela SObject Salesforce source...");
|
||||
var summariesTask = currentSalesforceSourceDiscovery.DiscoverEntitySummariesAsync();
|
||||
var entitiesTask = currentSalesforceSourceDiscovery.DiscoverEntitiesAsync();
|
||||
|
||||
// Le summaries sono rapide (1 sola API call) → rendiamo la UI interattiva subito
|
||||
salesforceSourceEntities = await summariesTask;
|
||||
isSalesforceSourceConnected = true;
|
||||
StateHasChanged();
|
||||
|
||||
Logger.LogInformation("SObject summaries caricate: {Count} entità disponibili", salesforceSourceEntities.Count);
|
||||
|
||||
// I dettagli completano in background (non bloccano la UI)
|
||||
try
|
||||
{
|
||||
await entitiesTask;
|
||||
Logger.LogInformation("Discovery dettagli SObject completata in background");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Impossibile completare la discovery dettagli SObject (non critico)");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
salesforceSourceErrorMessage = $"Errore di connessione: {ex.Message}";
|
||||
Logger.LogError(ex, "Errore nella connessione a Salesforce source: {Credential}", selectedSalesforceSourceCredential);
|
||||
currentSalesforceSourceClient = null;
|
||||
currentSalesforceSourceDiscovery = null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
isConnectingSalesforceSource = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seleziona un SObject Salesforce come sorgente dati e carica i suoi campi
|
||||
/// </summary>
|
||||
protected async Task SelectSalesforceSourceEntity(RestEntitySummary entity)
|
||||
{
|
||||
selectedSalesforceSourceEntity = entity;
|
||||
salesforceSourceEntityDetails = null;
|
||||
|
||||
// Carica i dettagli dei campi dell'SObject selezionato
|
||||
if (currentSalesforceSourceDiscovery != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInformation("Caricamento dettagli SObject sorgente: {EntityName}", entity.Name);
|
||||
salesforceSourceEntityDetails = await currentSalesforceSourceDiscovery.DiscoverEntityDetailsAsync(entity.Name);
|
||||
Logger.LogInformation("Dettagli SObject caricati: {FieldCount} campi disponibili",
|
||||
salesforceSourceEntityDetails?.Properties.Count ?? 0);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Errore nel caricamento dettagli SObject {EntityName}", entity.Name);
|
||||
salesforceSourceErrorMessage = $"Errore nel caricamento dei campi: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
// Pulisce i mapping esistenti quando si cambia entità
|
||||
ClearAllMappings();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restituisce la lista filtrata degli SObject in base al termine di ricerca
|
||||
/// </summary>
|
||||
protected IEnumerable<RestEntitySummary> GetFilteredSalesforceSourceEntities()
|
||||
{
|
||||
if (string.IsNullOrEmpty(salesforceSourceSearchTerm))
|
||||
return salesforceSourceEntities;
|
||||
|
||||
return salesforceSourceEntities
|
||||
.Where(e => e.Name.Contains(salesforceSourceSearchTerm, StringComparison.OrdinalIgnoreCase) ||
|
||||
(!string.IsNullOrEmpty(e.Label) && e.Label.Contains(salesforceSourceSearchTerm, StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggiorna il termine di ricerca per gli SObject sorgente
|
||||
/// </summary>
|
||||
protected void FilterSalesforceSourceEntities(ChangeEventArgs e)
|
||||
{
|
||||
salesforceSourceSearchTerm = e.Value?.ToString() ?? "";
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pulisce il termine di ricerca per gli SObject sorgente
|
||||
/// </summary>
|
||||
protected void ClearSalesforceSourceSearch()
|
||||
{
|
||||
salesforceSourceSearchTerm = "";
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Estrae tutti i record dall'SObject Salesforce selezionato usando le mappature campi configurate
|
||||
/// </summary>
|
||||
protected async Task<IEnumerable<Dictionary<string, object>>> GetAllRecordsFromSalesforceSource()
|
||||
{
|
||||
if (currentSalesforceSourceClient == null || selectedSalesforceSourceEntity == null)
|
||||
return new List<Dictionary<string, object>>();
|
||||
|
||||
if (!(currentSalesforceSourceClient is DataConnection.REST.Implementations.SalesforceServiceClient sfClient))
|
||||
{
|
||||
Logger.LogError("Il client Salesforce source non è un'istanza di SalesforceServiceClient");
|
||||
return new List<Dictionary<string, object>>();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Determina i campi da estrarre (solo quelli mappati + campo chiave)
|
||||
var fieldsToExtract = new List<string>();
|
||||
|
||||
// Aggiungi i campi sorgente dal mapping
|
||||
fieldsToExtract.AddRange(fieldMappings.Keys);
|
||||
|
||||
// Aggiungi il campo chiave sorgente se configurato
|
||||
if (!string.IsNullOrEmpty(sourceKeyField) && !fieldsToExtract.Contains(sourceKeyField))
|
||||
fieldsToExtract.Add(sourceKeyField);
|
||||
|
||||
// Aggiungi i campi usati nelle External ID Relationships (se presenti e destinazione è REST)
|
||||
foreach (var rel in externalIdRelationships)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(rel.SourceField) && !fieldsToExtract.Contains(rel.SourceField))
|
||||
fieldsToExtract.Add(rel.SourceField);
|
||||
}
|
||||
|
||||
// Se nessun campo è specificato, estrae tutto
|
||||
var fields = fieldsToExtract.Any() ? fieldsToExtract : null;
|
||||
|
||||
Logger.LogInformation("Estrazione dati da Salesforce SObject: {EntityName}, Campi: {Fields}",
|
||||
selectedSalesforceSourceEntity.Name,
|
||||
fields != null ? string.Join(", ", fields) : "tutti");
|
||||
|
||||
var records = await sfClient.ExtractAllEntitiesAsync(
|
||||
selectedSalesforceSourceEntity.Name,
|
||||
fields);
|
||||
|
||||
Logger.LogInformation("Estratti {Count} record da Salesforce {EntityName}",
|
||||
records.Count, selectedSalesforceSourceEntity.Name);
|
||||
|
||||
return records;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Errore nell'estrazione dati da Salesforce {EntityName}",
|
||||
selectedSalesforceSourceEntity?.Name ?? "N/A");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user