[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:
@@ -579,4 +579,94 @@ public class EFCoreDatabaseManager : IDatabaseManager
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<bool> UpsertRecordAsync(string tableName, string keyField, object? keyValue, Dictionary<string, object?> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,6 +85,18 @@ public interface IDatabaseManager : IDisposable
|
|||||||
/// Ottiene il nome del campo Primary Key di una tabella specifica
|
/// Ottiene il nome del campo Primary Key di una tabella specifica
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<string?> GetPrimaryKeyFieldAsync(string tableName);
|
Task<string?> GetPrimaryKeyFieldAsync(string tableName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tableName">Nome della tabella di destinazione</param>
|
||||||
|
/// <param name="keyField">Campo chiave per determinare se il record esiste</param>
|
||||||
|
/// <param name="keyValue">Valore del campo chiave del record</param>
|
||||||
|
/// <param name="record">Campi e valori da inserire/aggiornare</param>
|
||||||
|
/// <returns>True se l'operazione è riuscita, false altrimenti</returns>
|
||||||
|
Task<bool> UpsertRecordAsync(string tableName, string keyField, object? keyValue, Dictionary<string, object?> record);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -350,4 +350,61 @@ public class OdbcDatabaseManager : IDatabaseManager
|
|||||||
{
|
{
|
||||||
// Nessuna risorsa da rilasciare per ODBC diretto
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public partial class DataCoupler : ComponentBase
|
||||||
|
{
|
||||||
|
// ===== PROPRIETÀ TIPO DESTINAZIONE =====
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tipo di destinazione: "rest" (default) oppure "database".
|
||||||
|
/// Controlla quale sezione UI viene mostrata nella card di destra.
|
||||||
|
/// </summary>
|
||||||
|
protected string selectedDestinationType = "rest";
|
||||||
|
|
||||||
|
// ===== PROPRIETÀ DATABASE DESTINAZIONE =====
|
||||||
|
|
||||||
|
/// <summary>Credenziale database selezionata come destinazione</summary>
|
||||||
|
protected string selectedDestinationDatabaseCredential = "";
|
||||||
|
|
||||||
|
/// <summary>Stato connessione in corso</summary>
|
||||||
|
protected bool isConnectingDestinationDatabase = false;
|
||||||
|
|
||||||
|
/// <summary>Database di destinazione connesso con successo</summary>
|
||||||
|
protected bool isDestinationDatabaseConnected = false;
|
||||||
|
|
||||||
|
/// <summary>Messaggio di errore connessione database destinazione</summary>
|
||||||
|
protected string destinationDatabaseErrorMessage = "";
|
||||||
|
|
||||||
|
/// <summary>Nomi delle tabelle disponibili nel database di destinazione</summary>
|
||||||
|
protected List<string> destAvailableTableNames = new();
|
||||||
|
|
||||||
|
/// <summary>Schema dettagliato per tabella di destinazione (caricato on-demand)</summary>
|
||||||
|
protected Dictionary<string, IEnumerable<DbColumnInfo>> destDatabaseTables = new();
|
||||||
|
|
||||||
|
/// <summary>Tabella di destinazione selezionata</summary>
|
||||||
|
protected string selectedDestinationTable = "";
|
||||||
|
|
||||||
|
/// <summary>Termine di ricerca per filtrare le tabelle di destinazione</summary>
|
||||||
|
protected string destDatabaseSearchTerm = "";
|
||||||
|
|
||||||
|
/// <summary>Database manager per il database di destinazione</summary>
|
||||||
|
protected IDatabaseManager? currentDestinationDatabaseManager = null;
|
||||||
|
|
||||||
|
// ===== METODI DATABASE DESTINAZIONE =====
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gestisce il cambio del tipo di destinazione (rest / database)
|
||||||
|
/// </summary>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gestisce il cambio della credenziale database di destinazione
|
||||||
|
/// </summary>
|
||||||
|
protected void OnDestinationDatabaseCredentialChanged(ChangeEventArgs e)
|
||||||
|
{
|
||||||
|
selectedDestinationDatabaseCredential = e.Value?.ToString() ?? "";
|
||||||
|
ResetDestinationDatabaseState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resetta lo stato del database di destinazione
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Si connette al database di destinazione e carica le tabelle disponibili
|
||||||
|
/// </summary>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Seleziona la tabella di destinazione e carica il suo schema
|
||||||
|
/// </summary>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Restituisce la lista filtrata delle tabelle di destinazione
|
||||||
|
/// </summary>
|
||||||
|
protected IEnumerable<string> GetFilteredDestinationTables()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(destDatabaseSearchTerm))
|
||||||
|
return destAvailableTableNames;
|
||||||
|
|
||||||
|
return destAvailableTableNames
|
||||||
|
.Where(t => t.Contains(destDatabaseSearchTerm, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Aggiorna il termine di ricerca per le tabelle di destinazione
|
||||||
|
/// </summary>
|
||||||
|
protected void FilterDestinationTables(ChangeEventArgs e)
|
||||||
|
{
|
||||||
|
destDatabaseSearchTerm = e.Value?.ToString() ?? "";
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pulisce il termine di ricerca per le tabelle di destinazione
|
||||||
|
/// </summary>
|
||||||
|
protected void ClearDestinationTableSearch()
|
||||||
|
{
|
||||||
|
destDatabaseSearchTerm = "";
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indica se la configurazione corrente è pronta per il trasferimento verso database
|
||||||
|
/// </summary>
|
||||||
|
protected bool IsDestinationDatabaseReady =>
|
||||||
|
isDestinationDatabaseConnected &&
|
||||||
|
!string.IsNullOrEmpty(selectedDestinationTable) &&
|
||||||
|
currentDestinationDatabaseManager != null;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,6 +50,7 @@
|
|||||||
<option value="">-- Seleziona Tipo --</option>
|
<option value="">-- Seleziona Tipo --</option>
|
||||||
<option value="database">Database</option>
|
<option value="database">Database</option>
|
||||||
<option value="file">File (Excel/CSV)</option>
|
<option value="file">File (Excel/CSV)</option>
|
||||||
|
<option value="salesforce">Salesforce (REST API)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -709,17 +710,133 @@
|
|||||||
</div>
|
</div>
|
||||||
</div> }
|
</div> }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<!-- Sezione Salesforce come Fonte -->
|
||||||
|
@if (selectedSourceType == "salesforce")
|
||||||
|
{
|
||||||
|
<!-- Selezione Credenziali Salesforce -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Credenziali Salesforce:</label>
|
||||||
|
<select class="form-select" @onchange="OnSalesforceSourceCredentialChanged" value="@selectedSalesforceSourceCredential">
|
||||||
|
<option value="">-- Seleziona Salesforce --</option>
|
||||||
|
@foreach (var cred in salesforceSourceCredentials)
|
||||||
|
{
|
||||||
|
<option value="@cred.Name">@cred.Name (@cred.BaseUrl)</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(selectedSalesforceSourceCredential))
|
||||||
|
{
|
||||||
|
<div class="mb-3">
|
||||||
|
<button class="btn btn-success btn-sm" @onclick="ConnectToSalesforceSource" disabled="@isConnectingSalesforceSource">
|
||||||
|
@if (isConnectingSalesforceSource)
|
||||||
|
{
|
||||||
|
<span class="spinner-border spinner-border-sm me-2"></span>
|
||||||
|
}
|
||||||
|
<i class="fas fa-plug"></i> Connetti e Scopri SObject
|
||||||
|
</button>
|
||||||
|
@if (isSalesforceSourceConnected)
|
||||||
|
{
|
||||||
|
<span class="badge bg-success ms-2">Connesso</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(salesforceSourceErrorMessage))
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
@salesforceSourceErrorMessage
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Lista SObject Salesforce -->
|
||||||
|
@if (salesforceSourceEntities.Any())
|
||||||
|
{
|
||||||
|
<div class="mb-3">
|
||||||
|
<h6>SObject Salesforce (@salesforceSourceEntities.Count disponibili):</h6>
|
||||||
|
<div class="mb-2">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="fas fa-search"></i></span>
|
||||||
|
<input type="text" class="form-control" placeholder="Cerca SObject..."
|
||||||
|
@bind="salesforceSourceSearchTerm" @oninput="FilterSalesforceSourceEntities" />
|
||||||
|
@if (!string.IsNullOrEmpty(salesforceSourceSearchTerm))
|
||||||
|
{
|
||||||
|
<button class="btn btn-outline-secondary" @onclick="ClearSalesforceSourceSearch">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="list-group" style="max-height: 300px; overflow-y: auto;">
|
||||||
|
@foreach (var entity in GetFilteredSalesforceSourceEntities())
|
||||||
|
{
|
||||||
|
<a class="list-group-item list-group-item-action @(selectedSalesforceSourceEntity?.Name == entity.Name ? "active" : "")"
|
||||||
|
@onclick="@(async () => await SelectSalesforceSourceEntity(entity))">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<i class="fas fa-cloud"></i> @entity.Name
|
||||||
|
@if (!string.IsNullOrEmpty(entity.Label))
|
||||||
|
{
|
||||||
|
<small class="text-muted d-block">@entity.Label</small>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if (selectedSalesforceSourceEntity?.Name == entity.Name)
|
||||||
|
{
|
||||||
|
<span class="badge bg-primary">Selezionato</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if (!GetFilteredSalesforceSourceEntities().Any())
|
||||||
|
{
|
||||||
|
<div class="alert alert-info mt-2">
|
||||||
|
<i class="fas fa-info-circle"></i> Nessun SObject trovato con il termine "@salesforceSourceSearchTerm"
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Lato Destro - REST API -->
|
<!-- Lato Destro - Destinazione (REST API o Database) -->
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
<div class="card-header bg-info text-white">
|
<div class="card-header bg-info text-white">
|
||||||
<h5><i class="fas fa-cloud"></i> REST API Destination</h5>
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
@if (selectedDestinationType == "database")
|
||||||
|
{
|
||||||
|
<i class="fas fa-database"></i>
|
||||||
|
<span> Database Destination</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<i class="fas fa-cloud"></i>
|
||||||
|
<span> REST API Destination</span>
|
||||||
|
}
|
||||||
|
</h5>
|
||||||
|
<!-- Toggle Tipo Destinazione -->
|
||||||
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
|
<button type="button" class="btn @(selectedDestinationType == "rest" ? "btn-light" : "btn-outline-light")"
|
||||||
|
@onclick="@(() => OnDestinationTypeChanged(new ChangeEventArgs { Value = "rest" }))">
|
||||||
|
<i class="fas fa-cloud"></i> REST API
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn @(selectedDestinationType == "database" ? "btn-light" : "btn-outline-light")"
|
||||||
|
@onclick="@(() => OnDestinationTypeChanged(new ChangeEventArgs { Value = "database" }))">
|
||||||
|
<i class="fas fa-database"></i> Database
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
|
<!-- ===== DESTINAZIONE REST API ===== -->
|
||||||
|
@if (selectedDestinationType == "rest")
|
||||||
|
{
|
||||||
<!-- Selezione Credenziali REST -->
|
<!-- Selezione Credenziali REST -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Credenziali REST API:</label>
|
<label class="form-label">Credenziali REST API:</label>
|
||||||
@@ -807,19 +924,111 @@
|
|||||||
</div>
|
</div>
|
||||||
} </div>
|
} </div>
|
||||||
}
|
}
|
||||||
|
} @* fine @if (selectedDestinationType == "rest") *@
|
||||||
|
|
||||||
|
<!-- ===== DESTINAZIONE DATABASE ===== -->
|
||||||
|
@if (selectedDestinationType == "database")
|
||||||
|
{
|
||||||
|
<!-- Selezione Credenziali Database Destinazione -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Credenziali Database:</label>
|
||||||
|
<select class="form-select" @onchange="OnDestinationDatabaseCredentialChanged" value="@selectedDestinationDatabaseCredential">
|
||||||
|
<option value="">-- Seleziona Database --</option>
|
||||||
|
@foreach (var cred in databaseCredentials)
|
||||||
|
{
|
||||||
|
<option value="@cred.Name">@cred.Name (@cred.DatabaseType - @cred.Host)</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(selectedDestinationDatabaseCredential))
|
||||||
|
{
|
||||||
|
<div class="mb-3">
|
||||||
|
<button class="btn btn-success btn-sm" @onclick="ConnectToDestinationDatabase" disabled="@isConnectingDestinationDatabase">
|
||||||
|
@if (isConnectingDestinationDatabase)
|
||||||
|
{
|
||||||
|
<span class="spinner-border spinner-border-sm me-2"></span>
|
||||||
|
}
|
||||||
|
<i class="fas fa-plug"></i> Connetti e Scopri Tabelle
|
||||||
|
</button>
|
||||||
|
@if (isDestinationDatabaseConnected)
|
||||||
|
{
|
||||||
|
<span class="badge bg-success ms-2">Connesso</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(destinationDatabaseErrorMessage))
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
@destinationDatabaseErrorMessage
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Lista Tabelle Database Destinazione -->
|
||||||
|
@if (destAvailableTableNames.Any())
|
||||||
|
{
|
||||||
|
<div class="mb-3">
|
||||||
|
<h6>Tabelle Database (@destAvailableTableNames.Count disponibili):</h6>
|
||||||
|
<div class="mb-2">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="fas fa-search"></i></span>
|
||||||
|
<input type="text" class="form-control" placeholder="Cerca tabelle..."
|
||||||
|
@bind="destDatabaseSearchTerm" @oninput="FilterDestinationTables" />
|
||||||
|
@if (!string.IsNullOrEmpty(destDatabaseSearchTerm))
|
||||||
|
{
|
||||||
|
<button class="btn btn-outline-secondary" @onclick="ClearDestinationTableSearch">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="list-group" style="max-height: 300px; overflow-y: auto;">
|
||||||
|
@foreach (var table in GetFilteredDestinationTables())
|
||||||
|
{
|
||||||
|
<a class="list-group-item list-group-item-action @(selectedDestinationTable == table ? "active" : "")"
|
||||||
|
@onclick="@(async () => await SelectDestinationTable(table))">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<i class="fas fa-table"></i> @table
|
||||||
|
@if (destDatabaseTables.ContainsKey(table))
|
||||||
|
{
|
||||||
|
<small class="text-muted d-block">@destDatabaseTables[table].Count() colonne</small>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if (selectedDestinationTable == table)
|
||||||
|
{
|
||||||
|
<span class="badge bg-primary">Selezionata</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if (!GetFilteredDestinationTables().Any())
|
||||||
|
{
|
||||||
|
<div class="alert alert-info mt-2">
|
||||||
|
<i class="fas fa-info-circle"></i> Nessuna tabella trovata con "@destDatabaseSearchTerm"
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div> <!-- Sezione Mapping (quando la fonte è selezionata e REST è connesso) -->
|
</div> <!-- Sezione Mapping -->
|
||||||
@{
|
@{
|
||||||
// Per ODBC: non richiede isDatabaseConnected, basta query validata
|
// Per ODBC: non richiede isDatabaseConnected, basta query validata
|
||||||
// Per altri database: richiede connessione + (query validata OR tabella selezionata)
|
// Per altri database: richiede connessione + (query validata OR tabella selezionata)
|
||||||
var isSourceReady = (selectedSourceType == "database" &&
|
var isSourceReady = (selectedSourceType == "database" &&
|
||||||
((IsOdbcConnection() && useCustomQuery && isQueryValid) ||
|
((IsOdbcConnection() && useCustomQuery && isQueryValid) ||
|
||||||
(!IsOdbcConnection() && isDatabaseConnected && ((useCustomQuery && isQueryValid) || (!useCustomQuery && !string.IsNullOrEmpty(selectedTable)))))) ||
|
(!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)
|
||||||
{
|
{
|
||||||
<div class="row mt-4">
|
<div class="row mt-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
@@ -828,11 +1037,17 @@
|
|||||||
<h5><i class="fas fa-exchange-alt"></i> Mapping Campi</h5>
|
<h5><i class="fas fa-exchange-alt"></i> Mapping Campi</h5>
|
||||||
</div> <div class="card-body">
|
</div> <div class="card-body">
|
||||||
@{
|
@{
|
||||||
var sourceDisplayName = selectedSourceType == "database" ? selectedTable : selectedSheet;
|
var sourceDisplayName = selectedSourceType == "database" ? selectedTable :
|
||||||
var sourceTypeName = selectedSourceType == "database" ? "Tabella" : "Foglio";
|
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";
|
||||||
}
|
}
|
||||||
<h6>Mapping tra @sourceTypeName @sourceDisplayName e @selectedRestEntity.Name</h6>
|
<h6>Mapping tra @sourceTypeName @sourceDisplayName e @destDisplayName</h6>
|
||||||
<p class="text-muted">Configura il mapping tra i campi della fonte dati e le proprietà dell'entità REST</p>
|
<p class="text-muted">Configura il mapping tra i campi della fonte dati e le proprietà della destinazione</p>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- Colonna Sinistra: Campi Fonte -->
|
<!-- Colonna Sinistra: Campi Fonte -->
|
||||||
<div class="col-5">
|
<div class="col-5">
|
||||||
@@ -914,6 +1129,27 @@
|
|||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (selectedSourceType == "salesforce" && salesforceSourceEntityDetails != null)
|
||||||
|
{
|
||||||
|
@foreach (var field in salesforceSourceEntityDetails.Properties)
|
||||||
|
{
|
||||||
|
<a class="list-group-item list-group-item-action @(selectedDbColumn == field.Name ? "active" : "")"
|
||||||
|
@onclick="@(() => SelectDbColumn(field.Name))">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<strong>@field.Name</strong>
|
||||||
|
<small class="text-muted d-block">@field.Type</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
@if (fieldMappings.ContainsKey(field.Name))
|
||||||
|
{
|
||||||
|
<span class="badge bg-success">Mapped</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -998,9 +1234,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Colonna Destra: Proprietà REST -->
|
<!-- Colonna Destra: Proprietà Destinazione -->
|
||||||
<div class="col-5">
|
<div class="col-5">
|
||||||
<h6>Proprietà REST (@selectedRestEntity.Name)</h6>
|
@if (selectedDestinationType == "database" && !string.IsNullOrEmpty(selectedDestinationTable) && destDatabaseTables.ContainsKey(selectedDestinationTable))
|
||||||
|
{
|
||||||
|
<h6>Colonne Tabella (@selectedDestinationTable)</h6>
|
||||||
|
<div class="list-group" style="max-height: 400px; overflow-y: auto;">
|
||||||
|
@foreach (var col in destDatabaseTables[selectedDestinationTable])
|
||||||
|
{
|
||||||
|
<a class="list-group-item list-group-item-action @(selectedRestProperty == col.Name ? "active" : "")"
|
||||||
|
@onclick="@(() => SelectRestProperty(col.Name))">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<strong>@col.Name</strong>
|
||||||
|
<small class="text-muted d-block">@col.DataType</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
@if (col.IsPrimaryKey)
|
||||||
|
{
|
||||||
|
<span class="badge bg-warning text-dark">PK</span>
|
||||||
|
}
|
||||||
|
@if (fieldMappings.ContainsValue(col.Name))
|
||||||
|
{
|
||||||
|
<span class="badge bg-success">Mapped</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<h6>Proprietà REST (@(selectedRestEntity?.Name ?? ""))</h6>
|
||||||
<div class="list-group" style="max-height: 400px; overflow-y: auto;">
|
<div class="list-group" style="max-height: 400px; overflow-y: auto;">
|
||||||
@if (restEntityDetails != null)
|
@if (restEntityDetails != null)
|
||||||
{
|
{
|
||||||
@@ -1032,6 +1298,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ public partial class DataCoupler : ComponentBase
|
|||||||
{
|
{
|
||||||
databaseCredentials = await CredentialService.GetAllDatabaseCredentialsAsync();
|
databaseCredentials = await CredentialService.GetAllDatabaseCredentialsAsync();
|
||||||
await LoadRestCredentials(); // Carica le credenziali REST dalla classe parziale
|
await LoadRestCredentials(); // Carica le credenziali REST dalla classe parziale
|
||||||
|
await LoadSalesforceSourceCredentials(); // Carica le credenziali Salesforce per la fonte
|
||||||
// Carica anche i profili disponibili
|
// Carica anche i profili disponibili
|
||||||
await LoadProfiles();
|
await LoadProfiles();
|
||||||
}
|
}
|
||||||
@@ -809,6 +810,7 @@ public partial class DataCoupler : ComponentBase
|
|||||||
restSearchTerm = "";
|
restSearchTerm = "";
|
||||||
currentRestDiscovery = null;
|
currentRestDiscovery = null;
|
||||||
currentRestClient = null;
|
currentRestClient = null;
|
||||||
|
ResetDestinationDatabaseState();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnSourceTypeChanged(ChangeEventArgs e)
|
private void OnSourceTypeChanged(ChangeEventArgs e)
|
||||||
@@ -833,6 +835,9 @@ public partial class DataCoupler : ComponentBase
|
|||||||
fileData.Clear();
|
fileData.Clear();
|
||||||
selectedSheet = "";
|
selectedSheet = "";
|
||||||
|
|
||||||
|
// Reset Salesforce source state
|
||||||
|
ResetSalesforceSourceState();
|
||||||
|
|
||||||
// Reset pagination
|
// Reset pagination
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
|
|
||||||
@@ -1766,25 +1771,103 @@ public partial class DataCoupler : ComponentBase
|
|||||||
}
|
}
|
||||||
private async Task StartDataTransfer()
|
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)
|
if (currentRestClient is DataConnection.REST.Implementations.SalesforceServiceClient)
|
||||||
{
|
{
|
||||||
await StartDataTransferWithComposite();
|
await StartDataTransferWithComposite();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback al metodo originale per altri client REST
|
// Per altri client REST, usa il metodo originale
|
||||||
// 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
|
|
||||||
await StartDataTransferOriginal();
|
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()
|
private async Task StartDataTransferOriginal()
|
||||||
{
|
{
|
||||||
if (!fieldMappings.Any() || currentRestClient == null || selectedRestEntity == null)
|
if (!fieldMappings.Any() || currentRestClient == null || selectedRestEntity == null)
|
||||||
@@ -2248,6 +2331,10 @@ public partial class DataCoupler : ComponentBase
|
|||||||
{
|
{
|
||||||
return await GetAllRecordsFromFile();
|
return await GetAllRecordsFromFile();
|
||||||
}
|
}
|
||||||
|
else if (selectedSourceType == "salesforce")
|
||||||
|
{
|
||||||
|
return await GetAllRecordsFromSalesforceSource();
|
||||||
|
}
|
||||||
|
|
||||||
return new List<Dictionary<string, object>>();
|
return new List<Dictionary<string, object>>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": true,
|
"launchBrowser": true,
|
||||||
"applicationUrl": "http://localhost:5135",
|
"applicationUrl": "http://localhost:7550",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user