From a873dce31bef3a0b4704232d5ed2f6bd8664f16d Mon Sep 17 00:00:00 2001 From: Alessio Dal Santo Date: Tue, 17 Jun 2025 16:35:51 +0200 Subject: [PATCH] fix: Risolto errore "Invalid object name" nel trasferimento dati e pulizia codice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Modificato GetAllRecordsAsync per utilizzare la stessa connection string del discovery schema - Aggiunto metodo CreateConnection per creare connessioni DB appropriate per tipo - Migliorata gestione nomi tabelle con schema (es. "dbo.OCRD") - Rimossi metodi obsoleti di creazione entità (UpdateEntityData, CreateNewEntity) - Eliminati riferimenti a variabili non dichiarate (newEntityData, isCreatingEntity) - Aggiunto logging debug per connection string e query SQL - Completata implementazione trasferimento dati da database a REST API Il trasferimento dati ora utilizza la stessa connessione per discovery e estrazione, risolvendo problemi di accesso alle tabelle durante l'operazione di upsert. --- DataConnection/DB/EF/EFCoreDatabaseManager.cs | 100 +- .../SqlServerSchemaProvider.cs | 15 +- .../DB/Interfaces/IDatabaseManager.cs | 8 +- .../Implementations/BaseRestServiceClient.cs | 15 +- .../SalesforceServiceClient.cs | 59 + .../Implementations/SapB1ServiceClient.cs | 22 + .../REST/Interfaces/IRestServiceClient.cs | 22 +- Data_Coupler/Pages/DataCoupler.razor | 1017 +++++++++++++++++ Data_Coupler/Program.cs | 4 + .../Services/DataConnectionFactory.cs | 200 ++++ Data_Coupler/Shared/NavMenu.razor | 8 +- Data_Coupler/wwwroot/data/credentials.db | Bin 45056 -> 45056 bytes Data_Coupler/wwwroot/data/credentials.db-shm | Bin 32768 -> 0 bytes Data_Coupler/wwwroot/data/credentials.db-wal | 0 14 files changed, 1455 insertions(+), 15 deletions(-) create mode 100644 Data_Coupler/Pages/DataCoupler.razor create mode 100644 Data_Coupler/Services/DataConnectionFactory.cs delete mode 100644 Data_Coupler/wwwroot/data/credentials.db-shm delete mode 100644 Data_Coupler/wwwroot/data/credentials.db-wal 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 7e7649c76358452062120719a4dfeb150e922dd7..07d78473ca16895a7c8fdc286b7700f7c3c73fb8 100644 GIT binary patch delta 241 zcmZp8z|`=7X#-~gW9(+G0(U+}{>?#h+>AVp3XI|m^0M*_@=WrRi|UOzgPr_cQgib? zLnbe+cM~u)wlX%dGBDP&FfcbavNYfPyPi#epE;d@mBERD=O||vn?Fm&W`POe%q$EH zjEs}BC;ydfPT`Yekd|g(aA#l$Y!n5Fb1*a)J3CKqm>lTr8f;Y*QEcQ~Rb*~hli_@% delta 314 zcmZp8z|`=7X#-~gW7=k}0(U+}iOoTBB8)tZ;*8=9^0M*_@=UywGwY2fFRgbIFtD^T zHn%b|)H5z(64%tE6yG2-qpTcr6PMi3isYj52*ap=;HrwCFyG=R*YYCoG7oQ)OfRq@ zexA97=9#W;zNQsv=_!HkF1co|*`ZNI`C;M7ra?(z2Fc06;RY_5er1uFA^x8J!TwGe zWtQRE`G%(c{$7cxuA!xQ7G~*A=7}cB`o@XAM!^Bzp5~651v;1+C+kmUoh&_#YqP_& G1Cjtm4q*lW diff --git a/Data_Coupler/wwwroot/data/credentials.db-shm b/Data_Coupler/wwwroot/data/credentials.db-shm deleted file mode 100644 index 6e84bd6eaa9041700b62db2c14f115573d2a6467..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 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