diff --git a/DataConnection/DB/EF/EFCoreDatabaseManager.cs b/DataConnection/DB/EF/EFCoreDatabaseManager.cs index 4fcb5f8..6a54388 100644 --- a/DataConnection/DB/EF/EFCoreDatabaseManager.cs +++ b/DataConnection/DB/EF/EFCoreDatabaseManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Data; +using System.Data.Common; using System.Linq; using System.Linq.Expressions; using System.Threading.Tasks; @@ -8,6 +9,7 @@ using DataConnection.Interfaces; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.Data.SqlClient; namespace DataConnection.EF; @@ -103,27 +105,119 @@ public class EFCoreDatabaseManager : IDatabaseManager { return await _context.Database.ExecuteSqlRawAsync(sql, parameters); } - - public async Task>> GetDatabaseSchemaAsync() + public async Task>> GetDatabaseSchemaAsync() { try { + Console.WriteLine($"[DEBUG] Iniziando GetDatabaseSchemaAsync - DatabaseType: {_options.DatabaseType}"); + // Assicurarsi che il contesto sia connesso await _context.Database.OpenConnectionAsync(); + Console.WriteLine($"[DEBUG] Connessione al database aperta. Connection string: {_context.Database.GetConnectionString()}"); // Usa la factory per ottenere il provider appropriato in base al tipo di database var schemaProvider = DatabaseSchemaProviderFactory.CreateProvider(_options.DatabaseType); + Console.WriteLine($"[DEBUG] Schema provider creato: {schemaProvider.GetType().Name}"); // Usa il provider per ottenere lo schema - return await schemaProvider.GetDatabaseSchemaAsync(_context.Database.GetConnectionString()); + var result = await schemaProvider.GetDatabaseSchemaAsync(_context.Database.GetConnectionString()); + Console.WriteLine($"[DEBUG] Schema ottenuto. Numero tabelle: {result?.Count ?? 0}"); + + if (result != null && result.Count > 0) + { + foreach (var table in result.Take(3)) + { + Console.WriteLine($"[DEBUG] Tabella: {table.Key}, Colonne: {table.Value?.Count() ?? 0}"); + } + } + + return result; } catch (Exception ex) { Console.WriteLine($"Errore nel recupero dello schema del database: {ex.Message}"); + Console.WriteLine($"[DEBUG] Stack trace: {ex.StackTrace}"); + throw; } + } public async Task>> GetAllRecordsAsync(string tableName) + { + try + { + var records = new List>(); + + // Usa la stessa connection string utilizzata per il discovery dello schema + var connectionString = _context.Database.GetConnectionString(); + Console.WriteLine($"[DEBUG] GetAllRecordsAsync - Using connection string: {connectionString?.Substring(0, Math.Min(50, connectionString?.Length ?? 0))}..."); + + // Determina il tipo di connessione in base al DatabaseType + using var connection = CreateConnection(connectionString); + await connection.OpenAsync(); + + using var command = connection.CreateCommand(); + + // Query SQL semplice per ottenere tutti i record - limitiamo a 1000 per sicurezza + // Se il nome della tabella contiene già lo schema (es. "dbo.OCRD"), lo usiamo così com'è + // Altrimenti aggiungiamo le parentesi quadre + string tableReference; + if (tableName.Contains('.')) + { + // Il nome contiene già lo schema, separiamo e mettiamo entrambi tra parentesi quadre + var parts = tableName.Split('.'); + tableReference = $"[{parts[0]}].[{parts[1]}]"; + } + else + { + // Solo il nome della tabella, usiamo le parentesi quadre + tableReference = $"[{tableName}]"; + } + + command.CommandText = $"SELECT TOP 1000 * FROM {tableReference}"; + Console.WriteLine($"[DEBUG] GetAllRecordsAsync - Query: {command.CommandText}"); + + using var reader = await command.ExecuteReaderAsync(); + + while (await reader.ReadAsync()) + { + var record = new Dictionary(); + + for (int i = 0; i < reader.FieldCount; i++) + { + var columnName = reader.GetName(i); + var value = reader.IsDBNull(i) ? null : reader.GetValue(i); + record[columnName] = value; + } + + records.Add(record); + } + + Console.WriteLine($"[DEBUG] GetAllRecordsAsync - Tabella: {tableName}, Record ottenuti: {records.Count}"); + return records; + } + catch (Exception ex) + { + Console.WriteLine($"Errore nell'ottenere i record dalla tabella {tableName}: {ex.Message}"); throw; } } + /// + /// Crea una connessione database appropriata in base al tipo di database + /// + private DbConnection CreateConnection(string connectionString) + { + switch (_options.DatabaseType) + { + case Enums.DatabaseType.SqlServer: + return new SqlConnection(connectionString); + // Aggiungi altri tipi di database quando necessario + // case Enums.DatabaseType.MySQL: + // return new MySqlConnection(connectionString); + // case Enums.DatabaseType.PostgreSQL: + // return new NpgsqlConnection(connectionString); + default: + throw new NotSupportedException($"Database type {_options.DatabaseType} is not supported for direct connections"); + } + } + public void Dispose() { _context?.Dispose(); diff --git a/DataConnection/DB/EF/SchemaProviders/SqlServerSchemaProvider.cs b/DataConnection/DB/EF/SchemaProviders/SqlServerSchemaProvider.cs index 5672365..6ade431 100644 --- a/DataConnection/DB/EF/SchemaProviders/SqlServerSchemaProvider.cs +++ b/DataConnection/DB/EF/SchemaProviders/SqlServerSchemaProvider.cs @@ -11,16 +11,18 @@ namespace DataConnection.EF.SchemaProviders; /// Provider di schema per database SQL Server /// public class SqlServerSchemaProvider : IDatabaseSchemaProvider -{ - public async Task>> GetDatabaseSchemaAsync(string connectionString) +{ public async Task>> GetDatabaseSchemaAsync(string connectionString) { var result = new Dictionary>(); try { + Console.WriteLine($"[DEBUG] SqlServerSchemaProvider - Connection string: {connectionString?.Substring(0, Math.Min(50, connectionString?.Length ?? 0))}..."); + using (var connection = new SqlConnection(connectionString)) { await connection.OpenAsync(); + Console.WriteLine($"[DEBUG] SqlServerSchemaProvider - Connessione aperta"); // Query per ottenere la struttura delle tabelle in SQL Server string sql = @" @@ -110,12 +112,17 @@ public class SqlServerSchemaProvider : IDatabaseSchemaProvider columns?.Add(columnInfo); } - - // Aggiungiamo l'ultima tabella + // Aggiungiamo l'ultima tabella if (currentTable != null && columns != null && columns.Count > 0) { result[currentTable] = columns; } + + Console.WriteLine($"[DEBUG] SqlServerSchemaProvider - Query completata. Trovate {result.Count} tabelle"); + foreach (var table in result.Take(3)) + { + Console.WriteLine($"[DEBUG] SqlServerSchemaProvider - Tabella: {table.Key}, Colonne: {table.Value?.Count() ?? 0}"); + } } } } diff --git a/DataConnection/DB/Interfaces/IDatabaseManager.cs b/DataConnection/DB/Interfaces/IDatabaseManager.cs index f5e16f8..c5d96fe 100644 --- a/DataConnection/DB/Interfaces/IDatabaseManager.cs +++ b/DataConnection/DB/Interfaces/IDatabaseManager.cs @@ -45,11 +45,15 @@ public interface IDatabaseManager : IDisposable /// Esegue un comando SQL che non restituisce risultati /// Task ExecuteCommandAsync(string sql, params object[] parameters); - - /// + /// /// Ottiene i metadati delle tabelle nel database /// Task>> GetDatabaseSchemaAsync(); + + /// + /// Ottiene tutti i record da una tabella specifica come dizionari chiave-valore + /// + Task>> GetAllRecordsAsync(string tableName); } /// diff --git a/DataConnection/REST/Implementations/BaseRestServiceClient.cs b/DataConnection/REST/Implementations/BaseRestServiceClient.cs index 79b5c73..a094483 100644 --- a/DataConnection/REST/Implementations/BaseRestServiceClient.cs +++ b/DataConnection/REST/Implementations/BaseRestServiceClient.cs @@ -126,7 +126,20 @@ namespace DataConnection.REST.Implementations { Console.WriteLine($"Error during entity creation: {ex.Message}"); throw; - } + } } + + public virtual async Task?> UpsertEntityAsync(string entityName, Dictionary entityData, CancellationToken cancellationToken = default) + { + // Default implementation - just delegates to CreateEntityAsync + // Derived classes can override this for service-specific upsert logic + return await CreateEntityAsync(entityName, entityData, cancellationToken); + } + + public virtual async Task AuthenticateAsync(CancellationToken cancellationToken = default) + { + // Default implementation for basic authentication (already handled in ConfigureHttpClient) + // For services that require additional authentication steps, override this method + return await Task.FromResult(true); } // Implement other methods (PUT, DELETE, etc.) similarly diff --git a/DataConnection/REST/Implementations/SalesforceServiceClient.cs b/DataConnection/REST/Implementations/SalesforceServiceClient.cs index 2b5439a..c507c64 100644 --- a/DataConnection/REST/Implementations/SalesforceServiceClient.cs +++ b/DataConnection/REST/Implementations/SalesforceServiceClient.cs @@ -109,6 +109,35 @@ namespace DataConnection.REST.Implementations Console.WriteLine($"Error during Salesforce Authentication: {ex.Message}"); return false; } + } /// + /// Authenticates with Salesforce using the credentials from options. + /// + /// Cancellation token + /// True if authentication is successful + public override async Task AuthenticateAsync(CancellationToken cancellationToken = default) + { + // For Salesforce, we need ClientId, ClientSecret, Username, and Password + // These should be provided in the options + + if (string.IsNullOrEmpty(_options.Username) || string.IsNullOrEmpty(_options.Password)) + { + Console.WriteLine("Salesforce authentication requires username and password in options"); + return false; + } + + if (string.IsNullOrEmpty(_options.ApiKey) || string.IsNullOrEmpty(_options.AuthToken)) + { + Console.WriteLine("Salesforce authentication requires ApiKey (ClientId) and AuthToken (ClientSecret) in options"); + return false; + } + + // Use the actual credentials from options + var clientId = _options.ApiKey; // ClientId should be in ApiKey field + var clientSecret = _options.AuthToken; // ClientSecret should be in AuthToken field + + Console.WriteLine($"Using Salesforce credentials - ClientId: {clientId}, Username: {_options.Username}"); + + return await AuthenticateAsync(clientId, clientSecret, _options.Username, _options.Password, cancellationToken); } /// @@ -421,6 +450,36 @@ namespace DataConnection.REST.Implementations Console.WriteLine($"Error during Salesforce entity creation: {ex.Message}"); Console.WriteLine($"--- End Salesforce Entity Creation Attempt (Exception) ---"); return null; + } } + + public override async Task?> UpsertEntityAsync(string entityName, Dictionary entityData, CancellationToken cancellationToken = default) + { + // Per Salesforce, implementiamo upsert provando prima la creazione + // Se fallisce con un errore di duplicato, potremmo implementare logic di aggiornamento + // Per ora, semplicemente tentiamo la creazione + try + { + Console.WriteLine($"--- Starting Salesforce Entity Upsert: {entityName} ---"); + Console.WriteLine($"Entity Data: {string.Join(", ", entityData.Select(kvp => $"{kvp.Key}={kvp.Value}"))}"); + + // Prima tenta la creazione + var result = await CreateEntityAsync(entityName, entityData, cancellationToken); + + if (result != null) + { + Console.WriteLine($"Upsert completed successfully via CREATE for {entityName}"); + return result; + } + + // Se la creazione fallisce, potresti implementare qui la logica di aggiornamento + // Per ora, restituiamo null + Console.WriteLine($"Upsert failed for {entityName}"); + return null; + } + catch (Exception ex) + { + Console.WriteLine($"Error during Salesforce entity upsert: {ex.Message}"); + return null; } } diff --git a/DataConnection/REST/Implementations/SapB1ServiceClient.cs b/DataConnection/REST/Implementations/SapB1ServiceClient.cs index 1d79b2d..510b70e 100644 --- a/DataConnection/REST/Implementations/SapB1ServiceClient.cs +++ b/DataConnection/REST/Implementations/SapB1ServiceClient.cs @@ -540,6 +540,28 @@ namespace DataConnection.REST.Implementations } return null; + } /// + /// Authenticates with SAP B1 Service Layer using the credentials from options. + /// + /// Cancellation token + /// True if authentication is successful + public override async Task AuthenticateAsync(CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(_options.Username) || string.IsNullOrEmpty(_options.Password)) + { + Console.WriteLine("SAP B1 authentication requires username and password in options"); + return false; + } + + // For SAP B1, we also need the company database name + // We'll check multiple fields for the company DB name + var companyDB = !string.IsNullOrEmpty(_options.ApiKey) ? _options.ApiKey : + !string.IsNullOrEmpty(_options.AuthToken) ? _options.AuthToken : + "SBODEMOUS"; // Default fallback + + Console.WriteLine($"Using SAP B1 credentials - CompanyDB: {companyDB}, Username: {_options.Username}"); + + return await LoginAsync(companyDB, _options.Username, _options.Password, cancellationToken); } // Helper to get cookie value diff --git a/DataConnection/REST/Interfaces/IRestServiceClient.cs b/DataConnection/REST/Interfaces/IRestServiceClient.cs index cb11ebd..8d4db40 100644 --- a/DataConnection/REST/Interfaces/IRestServiceClient.cs +++ b/DataConnection/REST/Interfaces/IRestServiceClient.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Threading.Tasks; +using System.Threading; namespace DataConnection.REST.Interfaces { @@ -8,6 +9,14 @@ namespace DataConnection.REST.Interfaces /// public interface IRestServiceClient { + /// + /// Authenticates the client using the provided credentials. + /// Implementation varies by service type. + /// + /// Cancellation token. + /// True if authentication was successful, false otherwise. + Task AuthenticateAsync(CancellationToken cancellationToken = default); + /// /// Sends a GET request to the specified URI. /// @@ -26,9 +35,7 @@ namespace DataConnection.REST.Interfaces /// The HTTP request content sent to the server. /// Cancellation token. /// The deserialized response content. - Task PostAsync(string requestUri, TRequest payload, CancellationToken cancellationToken = default); - - /// + Task PostAsync(string requestUri, TRequest payload, CancellationToken cancellationToken = default); /// /// Creates a new entity by sending a POST request with the provided data. /// /// The name of the entity to create. @@ -37,6 +44,15 @@ namespace DataConnection.REST.Interfaces /// The created entity data or null if creation failed. Task?> CreateEntityAsync(string entityName, Dictionary entityData, CancellationToken cancellationToken = default); + /// + /// Creates a new entity or updates an existing one (upsert operation). + /// + /// The name of the entity to upsert. + /// The data for the entity as key-value pairs. + /// Cancellation token. + /// The upserted entity data or null if operation failed. + Task?> UpsertEntityAsync(string entityName, Dictionary entityData, CancellationToken cancellationToken = default); + // Add other methods as needed (PUT, DELETE, PATCH, etc.) // Consider adding methods for handling raw HttpResponseMessage or string responses } diff --git a/Data_Coupler/Pages/DataCoupler.razor b/Data_Coupler/Pages/DataCoupler.razor new file mode 100644 index 0000000..0d69450 --- /dev/null +++ b/Data_Coupler/Pages/DataCoupler.razor @@ -0,0 +1,1017 @@ +@page "/data-coupler" +@using CredentialManager.Models +@using DataConnection.Interfaces +@using DataConnection.CredentialManagement.Interfaces +@using DataConnection.REST.Interfaces +@using DataConnection.REST.Models +@using Data_Coupler.Services +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.JSInterop +@inject IDataConnectionCredentialService CredentialService +@inject IDataConnectionFactory ConnectionFactory +@inject IJSRuntime JSRuntime +@inject ILogger Logger + +Data Coupler + +
+
+
+

Data Coupler - Coupling Database e REST API

+

Connetti database e servizi REST per il trasferimento dati

+
+
+ +
+ +
+
+
+
Database Source
+
+
+ +
+ + +
+ + @if (!string.IsNullOrEmpty(selectedDatabaseCredential)) + { +
+ + @if (isDatabaseConnected) + { + Connesso + } +
+ } + + @if (!string.IsNullOrEmpty(databaseErrorMessage)) + { + + } + @if (databaseTables.Any()) + { +
+
Tabelle Database (@databaseTables.Count disponibili):
+ + +
+
+ + + + + @if (!string.IsNullOrEmpty(databaseSearchTerm)) + { + + } +
+
+ + + + + @if (!GetFilteredDatabaseTables().Any()) + { +
+ Nessuna tabella trovata con il termine di ricerca "@databaseSearchTerm" +
+ } +
+ } +
+
+
+ + +
+
+
+
REST API Destination
+
+
+ +
+ + +
+ + @if (!string.IsNullOrEmpty(selectedRestCredential)) + { +
+ + @if (isRestConnected) + { + Connesso + } +
+ } + + @if (!string.IsNullOrEmpty(restErrorMessage)) + { + + } + @if (restEntities.Any()) + { +
+
Entità REST (@restEntities.Count disponibili):
+ + +
+
+ + + + + @if (!string.IsNullOrEmpty(restSearchTerm)) + { + + } +
+
+ + + + + @if (!GetFilteredRestEntities().Any()) + { +
+ Nessuna entità trovata con il termine di ricerca "@restSearchTerm" +
+ }
+ } +
+
+
+
+ + + @if (isDatabaseConnected && isRestConnected && !string.IsNullOrEmpty(selectedTable) && selectedRestEntity != null) + { +
+
+
+
+
Mapping Campi
+
+
+
Mapping tra @selectedTable e @selectedRestEntity.Name
+

Configura il mapping tra i campi del database e le proprietà dell'entità REST

+
+ +
+
Campi Database (@selectedTable)
+
+ @if (databaseTables.ContainsKey(selectedTable)) + { + @foreach (var column in databaseTables[selectedTable]) + { + +
+
+ @column.Name + @column.DataType +
+
+ @if (column.IsPrimaryKey) + { + PK + } + @if (fieldMappings.ContainsKey(column.Name)) + { + Mapped + } +
+
+
+ } + } +
+
+ + +
+
+ + + + +
+
+ + +
+
Proprietà REST (@selectedRestEntity.Name)
+
+ @if (restEntityDetails != null) + { + @foreach (var property in restEntityDetails.Properties) + { + +
+
+ @property.Name + @property.Type +
+
+ @if (property.IsRequired) + { + Required + } + @if (fieldMappings.ContainsValue(property.Name)) + { + Mapped + } +
+
+
+ } + } +
+
+
+ + + @if (fieldMappings.Any()) + { +
+
Mappature Correnti (@fieldMappings.Count)
+
+ + + + + + + + + + + + + @foreach (var mapping in fieldMappings) + { + var dbColumn = databaseTables[selectedTable].FirstOrDefault(c => c.Name == mapping.Key); + var restProperty = restEntityDetails?.Properties.FirstOrDefault(p => p.Name == mapping.Value); + + + + + + + + + } + +
Campo DatabaseTipo DBProprietà RESTTipo RESTAzioni
@mapping.Key@(dbColumn?.DataType ?? "Unknown")@mapping.Value@(restProperty?.Type ?? "Unknown") + +
+
+
+ }
+
+
+ + + @if (fieldMappings.Any()) + { + + } +
+ +
+ @if (fieldMappings.Any()) + { + + @fieldMappings.Count mapping(s) configurati + + } + else + { + + Configura almeno una mappatura per iniziare + + } +
+
+ + @if (!string.IsNullOrEmpty(transferMessage)) + { + + } +
+
+
+
+
+ } +
+ +@code { + // Stato delle credenziali + private List databaseCredentials = new(); + private List restApiCredentials = new(); + + // Credenziali selezionate + private string selectedDatabaseCredential = ""; + private string selectedRestCredential = ""; + + // Stato connessioni + private bool isConnectingDatabase = false; + private bool isConnectingRest = false; + private bool isDatabaseConnected = false; + private bool isRestConnected = false; + + // Messaggi di errore + private string databaseErrorMessage = ""; + private string restErrorMessage = ""; + // Database discovery + private Dictionary> databaseTables = new(); + private string selectedTable = ""; + private string databaseSearchTerm = ""; + // REST discovery + private List restEntities = new(); + private RestEntitySummary? selectedRestEntity = null; + private RestEntityInfo? restEntityDetails = null; + private string restSearchTerm = ""; + + // Mapping campi + private Dictionary fieldMappings = new(); // DbColumn -> RestProperty + private string selectedDbColumn = ""; + private string selectedRestProperty = ""; + + // Trasferimento dati + private bool isTransferringData = false; + private string transferMessage = ""; + private string transferMessageType = ""; + + // Servizi + private IDatabaseManager? currentDatabaseManager = null; + private IRestMetadataDiscovery? currentRestDiscovery = null; + private IRestServiceClient? currentRestClient = null; + + protected override async Task OnInitializedAsync() + { + await LoadCredentials(); + } + + private async Task LoadCredentials() + { + try + { + databaseCredentials = await CredentialService.GetAllDatabaseCredentialsAsync(); + restApiCredentials = await CredentialService.GetAllRestApiCredentialsAsync(); + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nel caricamento delle credenziali"); + await JSRuntime.InvokeVoidAsync("alert", $"Errore nel caricamento delle credenziali: {ex.Message}"); + } + } private void OnDatabaseCredentialChanged(ChangeEventArgs e) + { + selectedDatabaseCredential = e.Value?.ToString() ?? ""; + ResetDatabaseState(); + } private void OnRestCredentialChanged(ChangeEventArgs e) + { + var newCredential = e.Value?.ToString() ?? ""; + + // Clear the cache if we're switching to a different credential + if (!string.IsNullOrEmpty(selectedRestCredential) && selectedRestCredential != newCredential) + { + ConnectionFactory.ClearRestClientCache(selectedRestCredential); + Logger.LogInformation("Cleared REST client cache for credential: {CredentialName}", selectedRestCredential); + } + + selectedRestCredential = newCredential; + ResetRestState(); + } private void ResetDatabaseState() + { + isDatabaseConnected = false; + databaseTables.Clear(); + selectedTable = ""; + databaseSearchTerm = ""; + databaseErrorMessage = ""; + currentDatabaseManager?.Dispose(); + currentDatabaseManager = null; + + // Clear mappings when resetting database state + ClearAllMappings(); + } private void ResetRestState() + { + isRestConnected = false; + restEntities.Clear(); + selectedRestEntity = null; + restEntityDetails = null; + restSearchTerm = ""; + restErrorMessage = ""; + currentRestDiscovery = null; + currentRestClient = null; + + // Clear mappings when resetting REST state + ClearAllMappings(); + }private async Task ConnectToDatabase() + { + if (string.IsNullOrEmpty(selectedDatabaseCredential)) + return; + + isConnectingDatabase = true; + databaseErrorMessage = ""; + + try + { // Trova la credenziale + var credential = databaseCredentials.FirstOrDefault(c => c.Name == selectedDatabaseCredential); + if (credential == null) + { + databaseErrorMessage = "Credenziale database non trovata"; + return; + } + + // Test della connessione + var (success, message) = await CredentialService.TestDatabaseConnectionAsync(credential.Name); + if (!success) + { + databaseErrorMessage = $"Connessione fallita: {message}"; + return; + } // Crea il database manager usando il factory con le credenziali complete + currentDatabaseManager = await ConnectionFactory.CreateDatabaseManagerAsync(selectedDatabaseCredential); + + Logger.LogInformation("Iniziando discovery dello schema per database {DatabaseType} con credenziale: {CredentialName}", credential.DatabaseType, selectedDatabaseCredential); + + // Discovery dello schema + var schema = await currentDatabaseManager.GetDatabaseSchemaAsync(); + + Logger.LogInformation("Schema discovery completato. Tipo restituito: {SchemaType}, Numero elementi: {Count}", + schema?.GetType().Name ?? "null", + schema?.Count() ?? 0); + + if (schema != null) + { + foreach (var item in schema.Take(5)) // Log primi 5 elementi per debug + { + Logger.LogInformation("Schema item - Key: {Key}, Value type: {ValueType}, Column count: {ColumnCount}", + item.Key, + item.Value?.GetType().Name ?? "null", + item.Value?.Count() ?? 0); + } + } + databaseTables = schema as Dictionary> ?? + (schema != null ? new Dictionary>(schema) : new Dictionary>()); + + Logger.LogInformation("Database tables dopo conversione: {Count} tabelle", databaseTables.Count); + + isDatabaseConnected = true; + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nella connessione al database"); + databaseErrorMessage = $"Errore: {ex.Message}"; + } + finally + { + isConnectingDatabase = false; + } + } private async Task ConnectToRestApi() + { + if (string.IsNullOrEmpty(selectedRestCredential)) + return; + + isConnectingRest = true; + restErrorMessage = ""; + + try + { + // Trova la credenziale + var credential = restApiCredentials.FirstOrDefault(c => c.Name == selectedRestCredential); + if (credential == null) + { + restErrorMessage = "Credenziale REST API non trovata"; + return; + } + + // Test della connessione + var (success, message) = await CredentialService.TestRestApiConnectionAsync(credential.Name); + if (!success) + { + restErrorMessage = $"Connessione fallita: {message}"; + return; + } // Crea i client REST usando il factory con le credenziali complete + currentRestClient = await ConnectionFactory.CreateRestServiceClientAsync(selectedRestCredential); + currentRestDiscovery = await ConnectionFactory.CreateRestMetadataDiscoveryAsync(selectedRestCredential); Logger.LogInformation("Iniziando autenticazione per il servizio REST {ServiceType} con credenziale: {CredentialName}", credential.ServiceType, selectedRestCredential); + + // Autenticazione prima del discovery + var authResult = await currentRestClient.AuthenticateAsync(); + if (!authResult) + { + Logger.LogWarning("Autenticazione fallita per il servizio REST {ServiceType}", credential.ServiceType); + restErrorMessage = "Autenticazione fallita per il servizio REST"; + return; + } + + Logger.LogInformation("Autenticazione completata. Iniziando discovery delle entità REST per {ServiceType}", credential.ServiceType); + + // Discovery delle entità disponibili + restEntities = await currentRestDiscovery.DiscoverEntitySummariesAsync(); + + Logger.LogInformation("Discovery completato. Trovate {Count} entità", restEntities?.Count ?? 0); + + if (restEntities == null || !restEntities.Any()) + { + Logger.LogWarning("Nessuna entità trovata dal servizio REST"); + restErrorMessage = "Nessuna entità disponibile dal servizio REST"; + return; + } + + isRestConnected = true; + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nella connessione al servizio REST"); + restErrorMessage = $"Errore: {ex.Message}"; + } + finally + { + isConnectingRest = false; + } + } private void SelectTable(string tableName) + { + selectedTable = tableName; + // Clear mappings when changing table + ClearAllMappings(); + } private async Task SelectRestEntity(RestEntitySummary entity) + { + selectedRestEntity = entity; + + // Clear mappings when changing entity + ClearAllMappings(); + + try + { + if (currentRestDiscovery != null) + { + // Discovery dei dettagli dell'entità + restEntityDetails = await currentRestDiscovery.DiscoverEntityDetailsAsync(entity.Name); } + else + { + restErrorMessage = "Servizio di discovery REST non disponibile"; + return; + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nel caricamento dettagli entità {EntityName}", entity.Name); + restErrorMessage = $"Errore nel caricamento dettagli entità: {ex.Message}"; + } } + + // Metodi per la ricerca e il filtraggio + private IEnumerable GetFilteredDatabaseTables() + { + if (string.IsNullOrEmpty(databaseSearchTerm)) + return databaseTables.Keys; + + return databaseTables.Keys.Where(table => + table.Contains(databaseSearchTerm, StringComparison.OrdinalIgnoreCase)); + } + + private IEnumerable GetFilteredRestEntities() + { + if (string.IsNullOrEmpty(restSearchTerm)) + return restEntities; + + return restEntities.Where(entity => + entity.Name.Contains(restSearchTerm, StringComparison.OrdinalIgnoreCase) || + (!string.IsNullOrEmpty(entity.Label) && entity.Label.Contains(restSearchTerm, StringComparison.OrdinalIgnoreCase))); + } + + private async Task FilterDatabaseTables(ChangeEventArgs e) + { + databaseSearchTerm = e.Value?.ToString() ?? ""; + await InvokeAsync(StateHasChanged); + } + + private async Task FilterRestEntities(ChangeEventArgs e) + { + restSearchTerm = e.Value?.ToString() ?? ""; + await InvokeAsync(StateHasChanged); + } + + private async Task ClearDatabaseSearch() + { + databaseSearchTerm = ""; + await InvokeAsync(StateHasChanged); + } + + private async Task ClearRestSearch() + { + restSearchTerm = ""; + await InvokeAsync(StateHasChanged); + } + + // Metodi per il mapping dei campi + private void SelectDbColumn(string columnName) + { + selectedDbColumn = columnName; + } + + private void SelectRestProperty(string propertyName) + { + selectedRestProperty = propertyName; + } + + private void CreateMapping() + { + if (string.IsNullOrEmpty(selectedDbColumn) || string.IsNullOrEmpty(selectedRestProperty)) + return; + + // Rimuovi eventuali mapping esistenti per questo campo database + if (fieldMappings.ContainsKey(selectedDbColumn)) + { + fieldMappings.Remove(selectedDbColumn); + } + + // Crea il nuovo mapping + fieldMappings[selectedDbColumn] = selectedRestProperty; + + Logger.LogInformation("Creato mapping: {DbColumn} -> {RestProperty}", selectedDbColumn, selectedRestProperty); + + // Deseleziona i campi + selectedDbColumn = ""; + selectedRestProperty = ""; + } + + private void RemoveMapping() + { + if (string.IsNullOrEmpty(selectedDbColumn) || !fieldMappings.ContainsKey(selectedDbColumn)) + return; + + fieldMappings.Remove(selectedDbColumn); + Logger.LogInformation("Rimosso mapping per campo: {DbColumn}", selectedDbColumn); + } + + private void RemoveSpecificMapping(string dbColumn) + { + if (fieldMappings.ContainsKey(dbColumn)) + { + fieldMappings.Remove(dbColumn); + Logger.LogInformation("Rimosso mapping specifico per campo: {DbColumn}", dbColumn); + } + } private void ClearAllMappings() + { + fieldMappings.Clear(); + selectedDbColumn = ""; + selectedRestProperty = ""; + transferMessage = ""; + transferMessageType = ""; + Logger.LogInformation("Tutti i mapping sono stati cancellati"); + } + + private void AutoMapFields() + { + if (!databaseTables.ContainsKey(selectedTable) || restEntityDetails == null) + return; + + var dbColumns = databaseTables[selectedTable]; + var restProperties = restEntityDetails.Properties; + + int mappingsCreated = 0; + + foreach (var dbColumn in dbColumns) + { + // Trova una proprietà REST con nome simile + var matchingProperty = restProperties.FirstOrDefault(p => + string.Equals(p.Name, dbColumn.Name, StringComparison.OrdinalIgnoreCase) || + string.Equals(p.Name.Replace("_", ""), dbColumn.Name.Replace("_", ""), StringComparison.OrdinalIgnoreCase) || + string.Equals(p.Name.Replace("Id", ""), dbColumn.Name.Replace("Id", ""), StringComparison.OrdinalIgnoreCase) + ); + + if (matchingProperty != null && !fieldMappings.ContainsKey(dbColumn.Name)) + { + fieldMappings[dbColumn.Name] = matchingProperty.Name; + mappingsCreated++; + } + } Logger.LogInformation("Auto-mapping completato. Creati {Count} mapping automatici", mappingsCreated); + } private async Task ShowMappingSummary() + { + var summary = "Riepilogo Mapping:\n\n"; + foreach (var mapping in fieldMappings) + { + summary += $"• {mapping.Key} → {mapping.Value}\n"; + } + + await JSRuntime.InvokeVoidAsync("alert", summary); + } + + private async Task StartDataTransfer() + { + if (!fieldMappings.Any() || currentDatabaseManager == null || currentRestClient == null || selectedRestEntity == null) + { + transferMessage = "Configurazione incompleta. Assicurati di aver selezionato tabella, entità e configurato almeno una mappatura."; + transferMessageType = "error"; + return; + } + + isTransferringData = true; + transferMessage = ""; + transferMessageType = ""; + + try + { + Logger.LogInformation("Iniziando trasferimento dati da {Table} a {Entity} con {MappingCount} mappature", + selectedTable, selectedRestEntity.Name, fieldMappings.Count); + + // 1. Ottieni tutti i record dalla tabella database + var records = await GetAllRecordsFromTable(); + Logger.LogInformation("Ottenuti {RecordCount} record dalla tabella {Table}", records.Count(), selectedTable); + + if (!records.Any()) + { + transferMessage = "Nessun record trovato nella tabella selezionata."; + transferMessageType = "error"; + return; + } + + // 2. Trasforma e trasferisci ogni record + int successCount = 0; + int errorCount = 0; + var errors = new List(); + + foreach (var record in records) + { + try + { + // Trasforma il record in base ai mapping + var restData = TransformRecordToRestEntity(record); + + // Esegui upsert (crea o aggiorna) + var result = await currentRestClient.UpsertEntityAsync(selectedRestEntity.Name, restData); + + if (result != null) + { + successCount++; + Logger.LogDebug("Record trasferito con successo: {Data}", string.Join(", ", restData.Select(kvp => $"{kvp.Key}={kvp.Value}"))); + } + else + { + errorCount++; + errors.Add($"Errore nel trasferimento del record (result null)"); + } + } + catch (Exception ex) + { + errorCount++; + errors.Add($"Errore nel trasferimento: {ex.Message}"); + Logger.LogError(ex, "Errore nel trasferimento di un record"); + } + } + + // 3. Mostra risultati + if (errorCount == 0) + { + transferMessage = $"Trasferimento completato con successo! {successCount} record trasferiti."; + transferMessageType = "success"; + } + else + { + transferMessage = $"Trasferimento completato con errori. Successi: {successCount}, Errori: {errorCount}. Primi errori: {string.Join("; ", errors.Take(3))}"; + transferMessageType = "error"; + } + + Logger.LogInformation("Trasferimento completato. Successi: {SuccessCount}, Errori: {ErrorCount}", successCount, errorCount); + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore generale nel trasferimento dati"); + transferMessage = $"Errore nel trasferimento dati: {ex.Message}"; + transferMessageType = "error"; + } + finally + { + isTransferringData = false; + } + } + + private async Task>> GetAllRecordsFromTable() + { + if (currentDatabaseManager == null || string.IsNullOrEmpty(selectedTable)) + return new List>(); + + try + { + // Usa il database manager per eseguire una query che ottiene tutti i record + // Questo è un esempio semplificato - potresti voler implementare paginazione per tabelle grandi + return await currentDatabaseManager.GetAllRecordsAsync(selectedTable); + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nell'ottenere i record dalla tabella {Table}", selectedTable); + throw; + } + } + + private Dictionary TransformRecordToRestEntity(Dictionary dbRecord) + { + var restData = new Dictionary(); + + foreach (var mapping in fieldMappings) + { + string dbColumn = mapping.Key; + string restProperty = mapping.Value; + + if (dbRecord.ContainsKey(dbColumn)) + { + var value = dbRecord[dbColumn]; + + // Trasforma il valore se necessario (es. date format, null handling, etc.) + var transformedValue = TransformValue(value, dbColumn, restProperty); + + if (transformedValue != null) + { + restData[restProperty] = transformedValue; + } + } + } + + Logger.LogDebug("Record trasformato: {DbColumns} → {RestProperties}", + string.Join(", ", dbRecord.Keys), + string.Join(", ", restData.Keys)); + + return restData; + } + + private object? TransformValue(object? value, string dbColumn, string restProperty) + { + if (value == null || value == DBNull.Value) + return null; + + // Ottieni informazioni sui tipi per fare trasformazioni intelligenti + var dbColumnInfo = databaseTables.ContainsKey(selectedTable) + ? databaseTables[selectedTable].FirstOrDefault(c => c.Name == dbColumn) + : null; + + var restPropertyInfo = restEntityDetails?.Properties.FirstOrDefault(p => p.Name == restProperty); + + // Trasformazioni specifiche per tipo + if (restPropertyInfo != null) + { + switch (restPropertyInfo.Type.ToLower()) + { + case "edm.string": + return value.ToString(); + + case "edm.int32": + case "edm.int64": + if (int.TryParse(value.ToString(), out int intVal)) + return intVal; + break; + + case "edm.decimal": + case "edm.double": + if (decimal.TryParse(value.ToString(), out decimal decVal)) + return decVal; + break; + + case "edm.boolean": + if (bool.TryParse(value.ToString(), out bool boolVal)) + return boolVal; + // Gestisci anche valori numerici (0/1) come boolean + if (value.ToString() == "1") return true; + if (value.ToString() == "0") return false; + break; + + case "edm.datetime": + case "edm.datetimeoffset": + if (DateTime.TryParse(value.ToString(), out DateTime dateVal)) + return dateVal.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); + break; + } + } + + // Fallback: restituisci il valore convertito a stringa + return value.ToString(); + } + + private string GetPropertyPlaceholder(RestPropertyInfo property) + { + return property.Type switch + { + "Edm.String" => $"Inserisci {property.Name}" + (property.MaxLength.HasValue ? $" (max {property.MaxLength})" : ""), + "Edm.Int32" => "Numero intero", + "Edm.Decimal" => "Numero decimale", + "Edm.DateTime" => "Data/Ora (YYYY-MM-DD)", + "Edm.Boolean" => "true/false", + _ => $"Valore per {property.Name}" + }; + } + + public void Dispose() + { + currentDatabaseManager?.Dispose(); + } +} diff --git a/Data_Coupler/Program.cs b/Data_Coupler/Program.cs index 1e47f71..505d922 100644 --- a/Data_Coupler/Program.cs +++ b/Data_Coupler/Program.cs @@ -7,6 +7,7 @@ using DataConnection.EF.DatabaseDiscovery; using DataConnection.Enums; using DataConnection.CredentialManagement; using CredentialManager; +using Data_Coupler.Services; using System; using System.Threading.Tasks; @@ -26,6 +27,9 @@ builder.Services.AddDataConnectionCredentialManagement($"Data Source={dbPath}"); // Register IHttpClientFactory builder.Services.AddHttpClient(); +// Register Data Connection Factory +builder.Services.AddScoped(); + var app = builder.Build(); // Initialize database diff --git a/Data_Coupler/Services/DataConnectionFactory.cs b/Data_Coupler/Services/DataConnectionFactory.cs new file mode 100644 index 0000000..819669c --- /dev/null +++ b/Data_Coupler/Services/DataConnectionFactory.cs @@ -0,0 +1,200 @@ +using CredentialManager.Models; +using DataConnection.EF; +using DataConnection.Interfaces; +using DataConnection.REST.Interfaces; +using DataConnection.REST.Implementations; +using DataConnection.REST.Configuration; +using DataConnection.CredentialManagement.Interfaces; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Data_Coupler.Services +{ + /// + /// Factory service per creare istanze di DatabaseManager e RestClient + /// + public interface IDataConnectionFactory + { + /// + /// Crea un DatabaseManager per la credenziale specificata + /// + Task CreateDatabaseManagerAsync(string credentialName); + + /// + /// Crea un RestServiceClient per la credenziale specificata + /// + Task CreateRestServiceClientAsync(string credentialName); + + /// + /// Crea un RestMetadataDiscovery per la credenziale specificata + /// + Task CreateRestMetadataDiscoveryAsync(string credentialName); + + /// + /// Pulisce la cache del client REST per una credenziale specifica + /// + void ClearRestClientCache(string credentialName); + /// + /// Pulisce tutta la cache dei client REST + /// + void ClearAllRestClientCache(); + } + + public class DataConnectionFactory : IDataConnectionFactory + { + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + private readonly IDataConnectionCredentialService _credentialService; + + // Cache per mantenere le istanze dei client REST autenticati + private readonly Dictionary _restClientCache = new(); + + public DataConnectionFactory(IServiceProvider serviceProvider, ILogger logger, IDataConnectionCredentialService credentialService) + { + _serviceProvider = serviceProvider; + _logger = logger; + _credentialService = credentialService; + }public async Task CreateDatabaseManagerAsync(string credentialName) + { + try + { + var credential = await _credentialService.GetDatabaseCredentialAsync(credentialName); + if (credential == null) + { + throw new ArgumentException($"Credenziale database '{credentialName}' non trovata"); + } + + var dbManagerOptions = await _credentialService.GetDbManagerOptionsAsync(credential.Name); + return new EFCoreDatabaseManager(dbManagerOptions); + } + catch (Exception ex) + { + _logger.LogError(ex, "Errore nella creazione del DatabaseManager per {CredentialName}", credentialName); + throw; + } + } public async Task CreateRestServiceClientAsync(string credentialName) + { + try + { + // Controlla se abbiamo già un client cached per questa credenziale + if (_restClientCache.TryGetValue(credentialName, out var cachedClient)) + { + _logger.LogInformation("Utilizzando client REST cached per {CredentialName}", credentialName); + return cachedClient; + } + + var credential = await _credentialService.GetRestApiCredentialAsync(credentialName); + if (credential == null) + { + throw new ArgumentException($"Credenziale REST API '{credentialName}' non trovata"); + } var options = new RestServiceOptions + { + BaseUrl = credential.BaseUrl, + Username = credential.Username, + Password = credential.Password, + TimeoutSeconds = credential.TimeoutSeconds, + IgnoreSslErrors = credential.IgnoreSslErrors + }; // Mapping specifico per tipo di servizio + switch (credential.ServiceType) + { + case RestServiceType.Salesforce: + // Per Salesforce usiamo i campi specifici ClientId e ClientSecret + options.ApiKey = credential.ClientId; // ClientId -> ApiKey + options.AuthToken = credential.ClientSecret; // ClientSecret -> AuthToken + _logger.LogInformation("Salesforce mapping - ClientId: {ClientId}, ClientSecret: {HasSecret}, Username: {Username}", + credential.ClientId, !string.IsNullOrEmpty(credential.ClientSecret), credential.Username); + break; + case RestServiceType.SapB1ServiceLayer: + // Per SAP B1 usiamo il CompanyDatabase come ApiKey + options.ApiKey = credential.CompanyDatabase; + options.AuthToken = credential.AuthToken; + _logger.LogInformation("SAP B1 mapping - CompanyDB: {CompanyDB}, Username: {Username}", + credential.CompanyDatabase, credential.Username); + break; + default: + // Per servizi generici usiamo ApiKey e AuthToken direttamente + options.ApiKey = credential.ApiKey; + options.AuthToken = credential.AuthToken; + _logger.LogInformation("Generic mapping - ApiKey: {ApiKey}, AuthToken: {HasToken}, Username: {Username}", + credential.ApiKey, !string.IsNullOrEmpty(credential.AuthToken), credential.Username); + break; + } + + var client = credential.ServiceType switch + { + RestServiceType.SapB1ServiceLayer => new SapB1ServiceClient(options), + RestServiceType.Salesforce => CreateSalesforceClient(options), + RestServiceType.Generic => CreateGenericRestClient(options), + _ => throw new NotSupportedException($"Tipo di servizio REST non supportato: {credential.ServiceType}") + }; + + // Salva il client nella cache + _restClientCache[credentialName] = client; + _logger.LogInformation("Client REST creato e cached per {CredentialName}", credentialName); + + return client; + } + catch (Exception ex) + { + _logger.LogError(ex, "Errore nella creazione del RestServiceClient per {CredentialName}", credentialName); + throw; + } + } public async Task CreateRestMetadataDiscoveryAsync(string credentialName) + { + try + { + // Utilizza lo stesso client REST cached (che è già autenticato) + var restClient = await CreateRestServiceClientAsync(credentialName); + + // I service client già implementano IRestMetadataDiscovery + if (restClient is IRestMetadataDiscovery metadataDiscovery) + { + _logger.LogInformation("Utilizzando lo stesso client REST per metadata discovery per {CredentialName}", credentialName); + return metadataDiscovery; + } + + var credential = await _credentialService.GetRestApiCredentialAsync(credentialName); + throw new NotSupportedException($"Il servizio REST {credential?.ServiceType} non supporta il metadata discovery"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Errore nella creazione del RestMetadataDiscovery per {CredentialName}", credentialName); + throw; + } + } + + private IRestServiceClient CreateGenericRestClient(RestServiceOptions options) + { + var httpClientFactory = _serviceProvider.GetRequiredService(); + var httpClient = httpClientFactory.CreateClient(); + return new BaseRestServiceClient(httpClient, options); + } + + private IRestServiceClient CreateSalesforceClient(RestServiceOptions options) + { + var httpClientFactory = _serviceProvider.GetRequiredService(); + var httpClient = httpClientFactory.CreateClient(); + return new SalesforceServiceClient(httpClient, options); + } + + /// + /// Pulisce la cache del client REST per una credenziale specifica + /// + public void ClearRestClientCache(string credentialName) + { + if (_restClientCache.Remove(credentialName)) + { + _logger.LogInformation("Cache del client REST rimossa per {CredentialName}", credentialName); + } + } + + /// + /// Pulisce tutta la cache dei client REST + /// + public void ClearAllRestClientCache() + { + _restClientCache.Clear(); + _logger.LogInformation("Cache di tutti i client REST pulita"); + } + } +} diff --git a/Data_Coupler/Shared/NavMenu.razor b/Data_Coupler/Shared/NavMenu.razor index 04da039..d932379 100644 --- a/Data_Coupler/Shared/NavMenu.razor +++ b/Data_Coupler/Shared/NavMenu.razor @@ -22,12 +22,16 @@ Fetch data - - + diff --git a/Data_Coupler/wwwroot/data/credentials.db b/Data_Coupler/wwwroot/data/credentials.db index 7e7649c..07d7847 100644 Binary files a/Data_Coupler/wwwroot/data/credentials.db and b/Data_Coupler/wwwroot/data/credentials.db differ diff --git a/Data_Coupler/wwwroot/data/credentials.db-shm b/Data_Coupler/wwwroot/data/credentials.db-shm deleted file mode 100644 index 6e84bd6..0000000 Binary files a/Data_Coupler/wwwroot/data/credentials.db-shm and /dev/null differ diff --git a/Data_Coupler/wwwroot/data/credentials.db-wal b/Data_Coupler/wwwroot/data/credentials.db-wal deleted file mode 100644 index e69de29..0000000