diff --git a/DataConnection/DB/EF/EFCoreDatabaseManager.cs b/DataConnection/DB/EF/EFCoreDatabaseManager.cs index 89678b5..596b67f 100644 --- a/DataConnection/DB/EF/EFCoreDatabaseManager.cs +++ b/DataConnection/DB/EF/EFCoreDatabaseManager.cs @@ -579,4 +579,94 @@ public class EFCoreDatabaseManager : IDatabaseManager return null; } } + + /// + public async Task UpsertRecordAsync(string tableName, string keyField, object? keyValue, Dictionary record) + { + try + { + if (_context.Database.GetDbConnection().State != ConnectionState.Open) + await _context.Database.OpenConnectionAsync(); + + var connection = _context.Database.GetDbConnection(); + + // Determina il riferimento alla tabella (con o senza schema) + string tableRef; + if (tableName.Contains('.')) + { + var parts = tableName.Split('.', 2); + tableRef = $"[{parts[0]}].[{parts[1]}]"; + } + else + { + tableRef = $"[{tableName}]"; + } + + // Controlla se il record esiste già + using var checkCmd = connection.CreateCommand(); + checkCmd.CommandText = $"SELECT COUNT(*) FROM {tableRef} WHERE [{keyField}] = @p0"; + var checkParam = checkCmd.CreateParameter(); + checkParam.ParameterName = "@p0"; + checkParam.Value = keyValue ?? DBNull.Value; + checkCmd.Parameters.Add(checkParam); + + 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, i) => $"[{f}] = @p{i}").ToList(); + var updateSql = $"UPDATE {tableRef} SET {string.Join(", ", setClauses)} WHERE [{keyField}] = @p{setClauses.Count}"; + + using var updateCmd = connection.CreateCommand(); + updateCmd.CommandText = updateSql; + + for (int i = 0; i < fields.Count; i++) + { + var p = updateCmd.CreateParameter(); + p.ParameterName = $"@p{i}"; + p.Value = record[fields[i]] ?? DBNull.Value; + updateCmd.Parameters.Add(p); + } + + // Aggiunge il parametro per la WHERE + var keyParam = updateCmd.CreateParameter(); + keyParam.ParameterName = $"@p{fields.Count}"; + keyParam.Value = keyValue ?? DBNull.Value; + updateCmd.Parameters.Add(keyParam); + + await updateCmd.ExecuteNonQueryAsync(); + } + else + { + // INSERT + var fields = record.Keys.ToList(); + var fieldNames = string.Join(", ", fields.Select(f => $"[{f}]")); + var paramPlaceholders = string.Join(", ", fields.Select((_, i) => $"@p{i}")); + var insertSql = $"INSERT INTO {tableRef} ({fieldNames}) VALUES ({paramPlaceholders})"; + + using var insertCmd = connection.CreateCommand(); + insertCmd.CommandText = insertSql; + + for (int i = 0; i < fields.Count; i++) + { + var p = insertCmd.CreateParameter(); + p.ParameterName = $"@p{i}"; + p.Value = record[fields[i]] ?? DBNull.Value; + insertCmd.Parameters.Add(p); + } + + await insertCmd.ExecuteNonQueryAsync(); + } + + return true; + } + catch (Exception ex) + { + Console.WriteLine($"Errore nell'upsert in {tableName}: {ex.Message}"); + return false; + } + } } diff --git a/DataConnection/DB/Interfaces/IDatabaseManager.cs b/DataConnection/DB/Interfaces/IDatabaseManager.cs index abc1a3a..1b31418 100644 --- a/DataConnection/DB/Interfaces/IDatabaseManager.cs +++ b/DataConnection/DB/Interfaces/IDatabaseManager.cs @@ -85,6 +85,18 @@ public interface IDatabaseManager : IDisposable /// Ottiene il nome del campo Primary Key di una tabella specifica /// Task GetPrimaryKeyFieldAsync(string tableName); + + /// + /// Esegue un upsert (INSERT o UPDATE) di un singolo record nella tabella specificata. + /// Se un record con lo stesso valore del campo chiave esiste già, viene aggiornato; + /// altrimenti viene inserito un nuovo record. + /// + /// Nome della tabella di destinazione + /// Campo chiave per determinare se il record esiste + /// Valore del campo chiave del record + /// Campi e valori da inserire/aggiornare + /// True se l'operazione è riuscita, false altrimenti + Task UpsertRecordAsync(string tableName, string keyField, object? keyValue, Dictionary record); } /// diff --git a/DataConnection/DB/OdbcDatabaseManager.cs b/DataConnection/DB/OdbcDatabaseManager.cs index 68bca5f..b5e2e62 100644 --- a/DataConnection/DB/OdbcDatabaseManager.cs +++ b/DataConnection/DB/OdbcDatabaseManager.cs @@ -350,4 +350,61 @@ public class OdbcDatabaseManager : IDatabaseManager { // Nessuna risorsa da rilasciare per ODBC diretto } + + /// + public async Task UpsertRecordAsync(string tableName, string keyField, object? keyValue, Dictionary 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; + } + } } diff --git a/Data_Coupler/Extensions/DataCoupler/DatabaseDestinationMethod.cs b/Data_Coupler/Extensions/DataCoupler/DatabaseDestinationMethod.cs new file mode 100644 index 0000000..0314f02 --- /dev/null +++ b/Data_Coupler/Extensions/DataCoupler/DatabaseDestinationMethod.cs @@ -0,0 +1,238 @@ +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.Interfaces; +using Data_Coupler.Services; + +namespace Data_Coupler.Pages; + +/// +/// Partial class per la gestione di un database come destinazione dati. +/// Consente di selezionare un database di destinazione, scoprirne le tabelle +/// e configurare il mapping dei campi verso la tabella di destinazione. +/// +public partial class DataCoupler : ComponentBase +{ + // ===== PROPRIETÀ TIPO DESTINAZIONE ===== + + /// + /// Tipo di destinazione: "rest" (default) oppure "database". + /// Controlla quale sezione UI viene mostrata nella card di destra. + /// + protected string selectedDestinationType = "rest"; + + // ===== PROPRIETÀ DATABASE DESTINAZIONE ===== + + /// Credenziale database selezionata come destinazione + protected string selectedDestinationDatabaseCredential = ""; + + /// Stato connessione in corso + protected bool isConnectingDestinationDatabase = false; + + /// Database di destinazione connesso con successo + protected bool isDestinationDatabaseConnected = false; + + /// Messaggio di errore connessione database destinazione + protected string destinationDatabaseErrorMessage = ""; + + /// Nomi delle tabelle disponibili nel database di destinazione + protected List destAvailableTableNames = new(); + + /// Schema dettagliato per tabella di destinazione (caricato on-demand) + protected Dictionary> destDatabaseTables = new(); + + /// Tabella di destinazione selezionata + protected string selectedDestinationTable = ""; + + /// Termine di ricerca per filtrare le tabelle di destinazione + protected string destDatabaseSearchTerm = ""; + + /// Database manager per il database di destinazione + protected IDatabaseManager? currentDestinationDatabaseManager = null; + + // ===== METODI DATABASE DESTINAZIONE ===== + + /// + /// Gestisce il cambio del tipo di destinazione (rest / database) + /// + protected void OnDestinationTypeChanged(ChangeEventArgs e) + { + var newType = e.Value?.ToString() ?? "rest"; + + if (newType == selectedDestinationType) + return; + + selectedDestinationType = newType; + + // Reset lo stato della destinazione precedente + ResetDestinationState(); + if (newType == "database") + { + ResetDestinationDatabaseState(); + } + + // Pulisce i mapping configurati (dipendono dal tipo di destinazione) + ClearAllMappings(); + StateHasChanged(); + } + + /// + /// Gestisce il cambio della credenziale database di destinazione + /// + protected void OnDestinationDatabaseCredentialChanged(ChangeEventArgs e) + { + selectedDestinationDatabaseCredential = e.Value?.ToString() ?? ""; + ResetDestinationDatabaseState(); + } + + /// + /// Resetta lo stato del database di destinazione + /// + protected void ResetDestinationDatabaseState() + { + isDestinationDatabaseConnected = false; + destAvailableTableNames.Clear(); + destDatabaseTables.Clear(); + selectedDestinationTable = ""; + destDatabaseSearchTerm = ""; + destinationDatabaseErrorMessage = ""; + + // Rilascia il database manager + if (currentDestinationDatabaseManager != null) + { + try { currentDestinationDatabaseManager.Dispose(); } catch { /* ignore */ } + currentDestinationDatabaseManager = null; + } + } + + /// + /// Si connette al database di destinazione e carica le tabelle disponibili + /// + protected async Task ConnectToDestinationDatabase() + { + if (string.IsNullOrEmpty(selectedDestinationDatabaseCredential)) + return; + + isConnectingDestinationDatabase = true; + destinationDatabaseErrorMessage = ""; + + try + { + // Verifica credenziale + var credential = databaseCredentials.FirstOrDefault(c => c.Name == selectedDestinationDatabaseCredential); + if (credential == null) + { + destinationDatabaseErrorMessage = "Credenziale database non trovata"; + return; + } + + // Crea il database manager + currentDestinationDatabaseManager = await ConnectionFactory.CreateDatabaseManagerAsync(selectedDestinationDatabaseCredential); + + // Verifica la connessione + var canConnect = await currentDestinationDatabaseManager.TestConnectionAsync(); + if (!canConnect) + { + destinationDatabaseErrorMessage = "Impossibile connettersi al database di destinazione. Verificare le credenziali."; + currentDestinationDatabaseManager.Dispose(); + currentDestinationDatabaseManager = null; + return; + } + + // Carica i nomi delle tabelle + var tableNames = await currentDestinationDatabaseManager.GetTableNamesAsync(); + destAvailableTableNames = tableNames.OrderBy(t => t).ToList(); + isDestinationDatabaseConnected = true; + + Logger.LogInformation("Database destinazione connesso: {Credential}, {Count} tabelle trovate", + selectedDestinationDatabaseCredential, destAvailableTableNames.Count); + } + catch (Exception ex) + { + destinationDatabaseErrorMessage = $"Errore di connessione: {ex.Message}"; + Logger.LogError(ex, "Errore nella connessione al database destinazione: {Credential}", selectedDestinationDatabaseCredential); + + if (currentDestinationDatabaseManager != null) + { + try { currentDestinationDatabaseManager.Dispose(); } catch { /* ignore */ } + currentDestinationDatabaseManager = null; + } + } + finally + { + isConnectingDestinationDatabase = false; + StateHasChanged(); + } + } + + /// + /// Seleziona la tabella di destinazione e carica il suo schema + /// + protected async Task SelectDestinationTable(string tableName) + { + selectedDestinationTable = tableName; + + // Carica lo schema della tabella se non è già disponibile + if (currentDestinationDatabaseManager != null && !destDatabaseTables.ContainsKey(tableName)) + { + try + { + Logger.LogInformation("Caricamento schema tabella destinazione: {TableName}", tableName); + var schema = await currentDestinationDatabaseManager.GetTableSchemaAsync(tableName); + destDatabaseTables[tableName] = schema; + Logger.LogInformation("Schema tabella destinazione caricato: {ColumnCount} colonne", schema.Count()); + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nel caricamento schema tabella destinazione {TableName}", tableName); + destinationDatabaseErrorMessage = $"Errore nel caricamento schema: {ex.Message}"; + } + } + + // Pulisce i mapping quando si cambia tabella + ClearAllMappings(); + StateHasChanged(); + } + + /// + /// Restituisce la lista filtrata delle tabelle di destinazione + /// + protected IEnumerable GetFilteredDestinationTables() + { + if (string.IsNullOrEmpty(destDatabaseSearchTerm)) + return destAvailableTableNames; + + return destAvailableTableNames + .Where(t => t.Contains(destDatabaseSearchTerm, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Aggiorna il termine di ricerca per le tabelle di destinazione + /// + protected void FilterDestinationTables(ChangeEventArgs e) + { + destDatabaseSearchTerm = e.Value?.ToString() ?? ""; + StateHasChanged(); + } + + /// + /// Pulisce il termine di ricerca per le tabelle di destinazione + /// + protected void ClearDestinationTableSearch() + { + destDatabaseSearchTerm = ""; + StateHasChanged(); + } + + /// + /// Indica se la configurazione corrente è pronta per il trasferimento verso database + /// + protected bool IsDestinationDatabaseReady => + isDestinationDatabaseConnected && + !string.IsNullOrEmpty(selectedDestinationTable) && + currentDestinationDatabaseManager != null; +} diff --git a/Data_Coupler/Extensions/DataCoupler/SalesforceSourceMethod.cs b/Data_Coupler/Extensions/DataCoupler/SalesforceSourceMethod.cs new file mode 100644 index 0000000..639b7af --- /dev/null +++ b/Data_Coupler/Extensions/DataCoupler/SalesforceSourceMethod.cs @@ -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; + +/// +/// 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. +/// +public partial class DataCoupler : ComponentBase +{ + // ===== PROPRIETÀ SALESFORCE SOURCE ===== + + /// Credenziali Salesforce disponibili come sorgente + protected List salesforceSourceCredentials = new(); + + /// Credenziale Salesforce selezionata come sorgente + protected string selectedSalesforceSourceCredential = ""; + + /// Stato connessione in corso + protected bool isConnectingSalesforceSource = false; + + /// Salesforce source connessa con successo + protected bool isSalesforceSourceConnected = false; + + /// Messaggio di errore connessione Salesforce source + protected string salesforceSourceErrorMessage = ""; + + /// Lista degli SObject Salesforce disponibili (summaries) + protected List salesforceSourceEntities = new(); + + /// SObject Salesforce selezionato come sorgente + protected RestEntitySummary? selectedSalesforceSourceEntity = null; + + /// Dettagli (campi) dell'SObject selezionato + protected RestEntityInfo? salesforceSourceEntityDetails = null; + + /// Termine di ricerca per filtrare gli SObject + protected string salesforceSourceSearchTerm = ""; + + /// Client REST per le operazioni Salesforce source + protected IRestServiceClient? currentSalesforceSourceClient = null; + + /// Discovery metadata per Salesforce source + protected IRestMetadataDiscovery? currentSalesforceSourceDiscovery = null; + + // ===== METODI SALESFORCE SOURCE ===== + + /// + /// Carica le credenziali di tipo Salesforce per usarle come sorgente + /// + 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"); + } + } + + /// + /// Gestisce il cambio di credenziale Salesforce source + /// + 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(); + } + + /// + /// Resetta lo stato della connessione Salesforce source + /// + protected void ResetSalesforceSourceState() + { + isSalesforceSourceConnected = false; + salesforceSourceEntities.Clear(); + selectedSalesforceSourceEntity = null; + salesforceSourceEntityDetails = null; + salesforceSourceSearchTerm = ""; + salesforceSourceErrorMessage = ""; + currentSalesforceSourceDiscovery = null; + currentSalesforceSourceClient = null; + } + + /// + /// Si connette a Salesforce come sorgente e scopre gli SObject disponibili + /// + 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(); + } + } + + /// + /// Seleziona un SObject Salesforce come sorgente dati e carica i suoi campi + /// + 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(); + } + + /// + /// Restituisce la lista filtrata degli SObject in base al termine di ricerca + /// + protected IEnumerable 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))); + } + + /// + /// Aggiorna il termine di ricerca per gli SObject sorgente + /// + protected void FilterSalesforceSourceEntities(ChangeEventArgs e) + { + salesforceSourceSearchTerm = e.Value?.ToString() ?? ""; + StateHasChanged(); + } + + /// + /// Pulisce il termine di ricerca per gli SObject sorgente + /// + protected void ClearSalesforceSourceSearch() + { + salesforceSourceSearchTerm = ""; + StateHasChanged(); + } + + /// + /// Estrae tutti i record dall'SObject Salesforce selezionato usando le mappature campi configurate + /// + protected async Task>> GetAllRecordsFromSalesforceSource() + { + if (currentSalesforceSourceClient == null || selectedSalesforceSourceEntity == null) + return new List>(); + + if (!(currentSalesforceSourceClient is DataConnection.REST.Implementations.SalesforceServiceClient sfClient)) + { + Logger.LogError("Il client Salesforce source non è un'istanza di SalesforceServiceClient"); + return new List>(); + } + + try + { + // Determina i campi da estrarre (solo quelli mappati + campo chiave) + var fieldsToExtract = new List(); + + // 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; + } + } +} diff --git a/Data_Coupler/Pages/DataCoupler.razor b/Data_Coupler/Pages/DataCoupler.razor index 9d956f8..840fb12 100644 --- a/Data_Coupler/Pages/DataCoupler.razor +++ b/Data_Coupler/Pages/DataCoupler.razor @@ -50,6 +50,7 @@ + @@ -709,17 +710,133 @@ } } + + + @if (selectedSourceType == "salesforce") + { + +
+ + +
+ + @if (!string.IsNullOrEmpty(selectedSalesforceSourceCredential)) + { +
+ + @if (isSalesforceSourceConnected) + { + Connesso + } +
+ } + + @if (!string.IsNullOrEmpty(salesforceSourceErrorMessage)) + { + + } + + + @if (salesforceSourceEntities.Any()) + { +
+
SObject Salesforce (@salesforceSourceEntities.Count disponibili):
+
+
+ + + @if (!string.IsNullOrEmpty(salesforceSourceSearchTerm)) + { + + } +
+
+ + @if (!GetFilteredSalesforceSourceEntities().Any()) + { +
+ Nessun SObject trovato con il termine "@salesforceSourceSearchTerm" +
+ } +
+ } + } - +
-
REST API Destination
+
+
+ @if (selectedDestinationType == "database") + { + + Database Destination + } + else + { + + REST API Destination + } +
+ +
+ + +
+
+ + + @if (selectedDestinationType == "rest") + {
@@ -807,19 +924,111 @@
}
} + } @* fine @if (selectedDestinationType == "rest") *@ + + + @if (selectedDestinationType == "database") + { + +
+ + +
+ + @if (!string.IsNullOrEmpty(selectedDestinationDatabaseCredential)) + { +
+ + @if (isDestinationDatabaseConnected) + { + Connesso + } +
+ } + + @if (!string.IsNullOrEmpty(destinationDatabaseErrorMessage)) + { + + } + + + @if (destAvailableTableNames.Any()) + { +
+
Tabelle Database (@destAvailableTableNames.Count disponibili):
+
+
+ + + @if (!string.IsNullOrEmpty(destDatabaseSearchTerm)) + { + + } +
+
+ + @if (!GetFilteredDestinationTables().Any()) + { +
+ Nessuna tabella trovata con "@destDatabaseSearchTerm" +
+ } +
+ } + }
- + @{ // Per ODBC: non richiede isDatabaseConnected, basta query validata // Per altri database: richiede connessione + (query validata OR tabella selezionata) - var isSourceReady = (selectedSourceType == "database" && - ((IsOdbcConnection() && useCustomQuery && isQueryValid) || + var isSourceReady = (selectedSourceType == "database" && + ((IsOdbcConnection() && useCustomQuery && isQueryValid) || (!IsOdbcConnection() && isDatabaseConnected && ((useCustomQuery && isQueryValid) || (!useCustomQuery && !string.IsNullOrEmpty(selectedTable)))))) || - (selectedSourceType == "file" && !string.IsNullOrEmpty(selectedSheet)); + (selectedSourceType == "file" && !string.IsNullOrEmpty(selectedSheet)) || + (selectedSourceType == "salesforce" && isSalesforceSourceConnected && selectedSalesforceSourceEntity != null); + var isDestinationReady = (selectedDestinationType == "rest" && isRestConnected && selectedRestEntity != null) || + (selectedDestinationType == "database" && IsDestinationDatabaseReady); } - @if (isSourceReady && isRestConnected && selectedRestEntity != null) + @if (isSourceReady && isDestinationReady) {
@@ -828,11 +1037,17 @@
Mapping Campi
@{ - var sourceDisplayName = selectedSourceType == "database" ? selectedTable : selectedSheet; - var sourceTypeName = selectedSourceType == "database" ? "Tabella" : "Foglio"; + var sourceDisplayName = selectedSourceType == "database" ? selectedTable : + selectedSourceType == "salesforce" ? selectedSalesforceSourceEntity?.Name ?? "" : + selectedSheet; + var sourceTypeName = selectedSourceType == "database" ? "Tabella" : + selectedSourceType == "salesforce" ? "SObject" : + "Foglio"; + var destDisplayName = selectedDestinationType == "database" ? selectedDestinationTable : selectedRestEntity?.Name ?? ""; + var destTypeName = selectedDestinationType == "database" ? "Tabella DB" : "Entità REST"; } -
Mapping tra @sourceTypeName @sourceDisplayName e @selectedRestEntity.Name
-

Configura il mapping tra i campi della fonte dati e le proprietà dell'entità REST

+
Mapping tra @sourceTypeName @sourceDisplayName e @destDisplayName
+

Configura il mapping tra i campi della fonte dati e le proprietà della destinazione

@@ -914,6 +1129,27 @@ } } + else if (selectedSourceType == "salesforce" && salesforceSourceEntityDetails != null) + { + @foreach (var field in salesforceSourceEntityDetails.Properties) + { + +
+
+ @field.Name + @field.Type +
+
+ @if (fieldMappings.ContainsKey(field.Name)) + { + Mapped + } +
+
+
+ } + }
@@ -998,40 +1234,71 @@
- +
-
Proprietà REST (@selectedRestEntity.Name)
-
- @if (restEntityDetails != null) - { - @foreach (var property in restEntityDetails.Properties) + @if (selectedDestinationType == "database" && !string.IsNullOrEmpty(selectedDestinationTable) && destDatabaseTables.ContainsKey(selectedDestinationTable)) + { +
Colonne Tabella (@selectedDestinationTable)
+ +
+ } + else + { +
Proprietà REST (@(selectedRestEntity?.Name ?? ""))
+ + }
diff --git a/Data_Coupler/Pages/DataCoupler.razor.cs b/Data_Coupler/Pages/DataCoupler.razor.cs index d6fcfa9..6d3361e 100644 --- a/Data_Coupler/Pages/DataCoupler.razor.cs +++ b/Data_Coupler/Pages/DataCoupler.razor.cs @@ -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(); + 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>(); } diff --git a/Data_Coupler/Properties/launchSettings.json b/Data_Coupler/Properties/launchSettings.json index 8b1b92a..76d99a6 100644 --- a/Data_Coupler/Properties/launchSettings.json +++ b/Data_Coupler/Properties/launchSettings.json @@ -12,7 +12,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "applicationUrl": "http://localhost:5135", + "applicationUrl": "http://localhost:7550", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }