a873dce31b
- 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.
1018 lines
46 KiB
Plaintext
1018 lines
46 KiB
Plaintext
@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<DataCoupler> Logger
|
|
|
|
<PageTitle>Data Coupler</PageTitle>
|
|
|
|
<div class="container-fluid">
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<h3><i class="fas fa-exchange-alt"></i> Data Coupler - Coupling Database e REST API</h3>
|
|
<p class="text-muted">Connetti database e servizi REST per il trasferimento dati</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<!-- Lato Sinistro - Database -->
|
|
<div class="col-md-6">
|
|
<div class="card h-100">
|
|
<div class="card-header bg-primary text-white">
|
|
<h5><i class="fas fa-database"></i> Database Source</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<!-- Selezione Credenziali Database -->
|
|
<div class="mb-3">
|
|
<label class="form-label">Credenziali Database:</label>
|
|
<select class="form-select" @onchange="OnDatabaseCredentialChanged" value="@selectedDatabaseCredential">
|
|
<option value="">-- Seleziona Database --</option>
|
|
@foreach (var cred in databaseCredentials)
|
|
{
|
|
<option value="@cred.Name">@cred.Name (@cred.DatabaseType - @cred.Host)</option>
|
|
}
|
|
</select>
|
|
</div>
|
|
|
|
@if (!string.IsNullOrEmpty(selectedDatabaseCredential))
|
|
{
|
|
<div class="mb-3">
|
|
<button class="btn btn-success btn-sm" @onclick="ConnectToDatabase" disabled="@isConnectingDatabase">
|
|
@if (isConnectingDatabase)
|
|
{
|
|
<span class="spinner-border spinner-border-sm me-2"></span>
|
|
}
|
|
<i class="fas fa-plug"></i> Connetti e Scopri Schema
|
|
</button>
|
|
@if (isDatabaseConnected)
|
|
{
|
|
<span class="badge bg-success ms-2">Connesso</span>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
@if (!string.IsNullOrEmpty(databaseErrorMessage))
|
|
{
|
|
<div class="alert alert-danger" role="alert">
|
|
@databaseErrorMessage
|
|
</div>
|
|
} <!-- Lista Tabelle -->
|
|
@if (databaseTables.Any())
|
|
{
|
|
<div class="mb-3">
|
|
<h6>Tabelle Database (@databaseTables.Count disponibili):</h6>
|
|
|
|
<!-- Campo di ricerca -->
|
|
<div class="mb-2">
|
|
<div class="input-group">
|
|
<span class="input-group-text">
|
|
<i class="fas fa-search"></i>
|
|
</span>
|
|
<input type="text" class="form-control" placeholder="Cerca tabelle..."
|
|
@bind="databaseSearchTerm" @oninput="FilterDatabaseTables" />
|
|
@if (!string.IsNullOrEmpty(databaseSearchTerm))
|
|
{
|
|
<button class="btn btn-outline-secondary" @onclick="ClearDatabaseSearch">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Lista tabelle filtrate -->
|
|
<div class="list-group" style="max-height: 300px; overflow-y: auto;">
|
|
@foreach (var table in GetFilteredDatabaseTables())
|
|
{
|
|
<a class="list-group-item list-group-item-action @(selectedTable == table ? "active" : "")"
|
|
@onclick="@(() => SelectTable(table))">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<i class="fas fa-table"></i> @table
|
|
@if (databaseTables.ContainsKey(table))
|
|
{
|
|
<small class="text-muted d-block">@databaseTables[table].Count() campi</small>
|
|
}
|
|
</div>
|
|
@if (selectedTable == table)
|
|
{
|
|
<span class="badge bg-primary">Selezionata</span>
|
|
}
|
|
</div>
|
|
</a>
|
|
}
|
|
</div>
|
|
|
|
@if (!GetFilteredDatabaseTables().Any())
|
|
{
|
|
<div class="alert alert-info mt-2">
|
|
<i class="fas fa-info-circle"></i> Nessuna tabella trovata con il termine di ricerca "@databaseSearchTerm"
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Lato Destro - REST API -->
|
|
<div class="col-md-6">
|
|
<div class="card h-100">
|
|
<div class="card-header bg-info text-white">
|
|
<h5><i class="fas fa-cloud"></i> REST API Destination</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<!-- Selezione Credenziali REST -->
|
|
<div class="mb-3">
|
|
<label class="form-label">Credenziali REST API:</label>
|
|
<select class="form-select" @onchange="OnRestCredentialChanged" value="@selectedRestCredential">
|
|
<option value="">-- Seleziona REST API --</option>
|
|
@foreach (var cred in restApiCredentials)
|
|
{
|
|
<option value="@cred.Name">@cred.Name (@cred.ServiceType - @cred.BaseUrl)</option>
|
|
}
|
|
</select>
|
|
</div>
|
|
|
|
@if (!string.IsNullOrEmpty(selectedRestCredential))
|
|
{
|
|
<div class="mb-3">
|
|
<button class="btn btn-success btn-sm" @onclick="ConnectToRestApi" disabled="@isConnectingRest">
|
|
@if (isConnectingRest)
|
|
{
|
|
<span class="spinner-border spinner-border-sm me-2"></span>
|
|
}
|
|
<i class="fas fa-plug"></i> Connetti e Scopri Entità
|
|
</button>
|
|
@if (isRestConnected)
|
|
{
|
|
<span class="badge bg-success ms-2">Connesso</span>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
@if (!string.IsNullOrEmpty(restErrorMessage))
|
|
{
|
|
<div class="alert alert-danger" role="alert">
|
|
@restErrorMessage
|
|
</div>
|
|
} <!-- Lista Entità REST -->
|
|
@if (restEntities.Any())
|
|
{
|
|
<div class="mb-3">
|
|
<h6>Entità REST (@restEntities.Count disponibili):</h6>
|
|
|
|
<!-- Campo di ricerca -->
|
|
<div class="mb-2">
|
|
<div class="input-group">
|
|
<span class="input-group-text">
|
|
<i class="fas fa-search"></i>
|
|
</span>
|
|
<input type="text" class="form-control" placeholder="Cerca entità..."
|
|
@bind="restSearchTerm" @oninput="FilterRestEntities" />
|
|
@if (!string.IsNullOrEmpty(restSearchTerm))
|
|
{
|
|
<button class="btn btn-outline-secondary" @onclick="ClearRestSearch">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Lista entità filtrate -->
|
|
<div class="list-group" style="max-height: 300px; overflow-y: auto;">
|
|
@foreach (var entity in GetFilteredRestEntities())
|
|
{
|
|
<a class="list-group-item list-group-item-action @(selectedRestEntity?.Name == entity.Name ? "active" : "")"
|
|
@onclick="@(() => SelectRestEntity(entity))">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<i class="fas fa-cube"></i> @entity.Name
|
|
@if (!string.IsNullOrEmpty(entity.Label))
|
|
{
|
|
<small class="text-muted d-block">@entity.Label</small>
|
|
}
|
|
</div>
|
|
@if (selectedRestEntity?.Name == entity.Name)
|
|
{
|
|
<span class="badge bg-primary">Selezionata</span>
|
|
}
|
|
</div>
|
|
</a>
|
|
}
|
|
</div>
|
|
|
|
@if (!GetFilteredRestEntities().Any())
|
|
{
|
|
<div class="alert alert-info mt-2">
|
|
<i class="fas fa-info-circle"></i> Nessuna entità trovata con il termine di ricerca "@restSearchTerm"
|
|
</div>
|
|
} </div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sezione Mapping (quando entrambi sono connessi) -->
|
|
@if (isDatabaseConnected && isRestConnected && !string.IsNullOrEmpty(selectedTable) && selectedRestEntity != null)
|
|
{
|
|
<div class="row mt-4">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header bg-success text-white">
|
|
<h5><i class="fas fa-exchange-alt"></i> Mapping Campi</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<h6>Mapping tra @selectedTable e @selectedRestEntity.Name</h6>
|
|
<p class="text-muted">Configura il mapping tra i campi del database e le proprietà dell'entità REST</p>
|
|
<div class="row">
|
|
<!-- Colonna Sinistra: Campi Database -->
|
|
<div class="col-5">
|
|
<h6>Campi Database (@selectedTable)</h6>
|
|
<div class="list-group" style="max-height: 400px; overflow-y: auto;">
|
|
@if (databaseTables.ContainsKey(selectedTable))
|
|
{
|
|
@foreach (var column in databaseTables[selectedTable])
|
|
{
|
|
<a class="list-group-item list-group-item-action @(selectedDbColumn == column.Name ? "active" : "")"
|
|
@onclick="@(() => SelectDbColumn(column.Name))">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<strong>@column.Name</strong>
|
|
<small class="text-muted d-block">@column.DataType</small>
|
|
</div>
|
|
<div>
|
|
@if (column.IsPrimaryKey)
|
|
{
|
|
<span class="badge bg-primary">PK</span>
|
|
}
|
|
@if (fieldMappings.ContainsKey(column.Name))
|
|
{
|
|
<span class="badge bg-success">Mapped</span>
|
|
}
|
|
</div>
|
|
</div>
|
|
</a>
|
|
}
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Colonna Centrale: Controlli Mapping -->
|
|
<div class="col-2 text-center">
|
|
<div class="d-flex flex-column justify-content-center h-100">
|
|
<button class="btn btn-success mb-2" @onclick="CreateMapping"
|
|
disabled="@(string.IsNullOrEmpty(selectedDbColumn) || string.IsNullOrEmpty(selectedRestProperty))">
|
|
<i class="fas fa-arrow-right"></i>
|
|
<small class="d-block">Map</small>
|
|
</button>
|
|
<button class="btn btn-danger mb-2" @onclick="RemoveMapping"
|
|
disabled="@(string.IsNullOrEmpty(selectedDbColumn) || !fieldMappings.ContainsKey(selectedDbColumn))">
|
|
<i class="fas fa-times"></i>
|
|
<small class="d-block">Remove</small>
|
|
</button>
|
|
<button class="btn btn-warning mb-2" @onclick="AutoMapFields">
|
|
<i class="fas fa-magic"></i>
|
|
<small class="d-block">Auto</small>
|
|
</button>
|
|
<button class="btn btn-secondary" @onclick="ClearAllMappings">
|
|
<i class="fas fa-trash"></i>
|
|
<small class="d-block">Clear</small>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Colonna Destra: Proprietà REST -->
|
|
<div class="col-5">
|
|
<h6>Proprietà REST (@selectedRestEntity.Name)</h6>
|
|
<div class="list-group" style="max-height: 400px; overflow-y: auto;">
|
|
@if (restEntityDetails != null)
|
|
{
|
|
@foreach (var property in restEntityDetails.Properties)
|
|
{
|
|
<a class="list-group-item list-group-item-action @(selectedRestProperty == property.Name ? "active" : "")"
|
|
@onclick="@(() => SelectRestProperty(property.Name))">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<strong>@property.Name</strong>
|
|
<small class="text-muted d-block">@property.Type</small>
|
|
</div>
|
|
<div>
|
|
@if (property.IsRequired)
|
|
{
|
|
<span class="badge bg-danger">Required</span>
|
|
}
|
|
@if (fieldMappings.ContainsValue(property.Name))
|
|
{
|
|
<span class="badge bg-success">Mapped</span>
|
|
}
|
|
</div>
|
|
</div>
|
|
</a>
|
|
}
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sezione Mappature Correnti -->
|
|
@if (fieldMappings.Any())
|
|
{
|
|
<div class="mt-4">
|
|
<h6>Mappature Correnti (@fieldMappings.Count)</h6>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm table-striped">
|
|
<thead>
|
|
<tr>
|
|
<th>Campo Database</th>
|
|
<th>Tipo DB</th>
|
|
<th>→</th>
|
|
<th>Proprietà REST</th>
|
|
<th>Tipo REST</th>
|
|
<th>Azioni</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@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);
|
|
<tr>
|
|
<td><strong>@mapping.Key</strong></td>
|
|
<td><small class="text-muted">@(dbColumn?.DataType ?? "Unknown")</small></td>
|
|
<td><i class="fas fa-arrow-right text-success"></i></td>
|
|
<td><strong>@mapping.Value</strong></td>
|
|
<td><small class="text-muted">@(restProperty?.Type ?? "Unknown")</small></td>
|
|
<td>
|
|
<button class="btn btn-sm btn-danger" @onclick="@(() => RemoveSpecificMapping(mapping.Key))">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
} <div class="mt-3">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<button class="btn btn-success" @onclick="StartDataTransfer" disabled="@(!fieldMappings.Any() || isTransferringData)"> @if (isTransferringData)
|
|
{
|
|
<span class="spinner-border spinner-border-sm me-2"></span>
|
|
<i class="fas fa-sync-alt"></i> @("Trasferimento in corso")
|
|
}
|
|
else
|
|
{
|
|
<i class="fas fa-play"></i> @("Avvia Trasferimento Dati")
|
|
}
|
|
</button>
|
|
|
|
@if (fieldMappings.Any())
|
|
{
|
|
<button class="btn btn-info ms-2" @onclick="ShowMappingSummary">
|
|
<i class="fas fa-list"></i> Riepilogo Mapping
|
|
</button>
|
|
}
|
|
</div>
|
|
|
|
<div class="text-muted">
|
|
@if (fieldMappings.Any())
|
|
{
|
|
<small>
|
|
<i class="fas fa-arrow-right"></i> @fieldMappings.Count mapping(s) configurati
|
|
</small>
|
|
}
|
|
else
|
|
{
|
|
<small>
|
|
<i class="fas fa-exclamation-triangle"></i> Configura almeno una mappatura per iniziare
|
|
</small>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
@if (!string.IsNullOrEmpty(transferMessage))
|
|
{
|
|
<div class="alert @(transferMessageType == "success" ? "alert-success" : "alert-danger") mt-3" role="alert">
|
|
<i class="fas @(transferMessageType == "success" ? "fa-check-circle" : "fa-exclamation-circle")"></i>
|
|
@transferMessage
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
@code {
|
|
// Stato delle credenziali
|
|
private List<DatabaseCredential> databaseCredentials = new();
|
|
private List<RestApiCredential> 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<string, IEnumerable<DbColumnInfo>> databaseTables = new();
|
|
private string selectedTable = "";
|
|
private string databaseSearchTerm = "";
|
|
// REST discovery
|
|
private List<RestEntitySummary> restEntities = new();
|
|
private RestEntitySummary? selectedRestEntity = null;
|
|
private RestEntityInfo? restEntityDetails = null;
|
|
private string restSearchTerm = "";
|
|
|
|
// Mapping campi
|
|
private Dictionary<string, string> 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<string, IEnumerable<DbColumnInfo>> ??
|
|
(schema != null ? new Dictionary<string, IEnumerable<DbColumnInfo>>(schema) : new Dictionary<string, IEnumerable<DbColumnInfo>>());
|
|
|
|
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<string> GetFilteredDatabaseTables()
|
|
{
|
|
if (string.IsNullOrEmpty(databaseSearchTerm))
|
|
return databaseTables.Keys;
|
|
|
|
return databaseTables.Keys.Where(table =>
|
|
table.Contains(databaseSearchTerm, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
private IEnumerable<RestEntitySummary> 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<string>();
|
|
|
|
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<IEnumerable<Dictionary<string, object>>> GetAllRecordsFromTable()
|
|
{
|
|
if (currentDatabaseManager == null || string.IsNullOrEmpty(selectedTable))
|
|
return new List<Dictionary<string, object>>();
|
|
|
|
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<string, object> TransformRecordToRestEntity(Dictionary<string, object> dbRecord)
|
|
{
|
|
var restData = new Dictionary<string, object>();
|
|
|
|
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();
|
|
}
|
|
}
|