feat: refactoring DataCoupler - suddivisione in classi parziali
- Estrazione logica database in DatabaseMethod.cs con tutte le proprietà e metodi protected - Estrazione logica REST API in RESTMethod.cs con gestione completa delle connessioni - Creazione DataCouplerModels.cs per modelli condivisi (TransferResult) - Pulizia file principale DataCoupler.razor.cs mantenendo solo orchestrazione generale - Rimozione codice duplicato e miglioramento separazione delle responsabilità - Compilazione verificata senza errori per l'intera soluzione Struttura finale: - DataCoupler.razor.cs: gestione file, profili, UI e coordinamento - DatabaseMethod.cs: connessioni DB, query custom, discovery tabelle/schemi - RESTMethod.cs: autenticazione REST, discovery entità, metadata - DataCouplerModels.cs: modelli comuni per
This commit is contained in:
@@ -0,0 +1,882 @@
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.Text;
|
||||
using CredentialManager.Models;
|
||||
using CredentialManager.Services;
|
||||
using DataConnection.Interfaces;
|
||||
using DataConnection.REST.Interfaces;
|
||||
using DataConnection.REST.Models;
|
||||
using DataConnection.CredentialManagement.Interfaces;
|
||||
using ExcelDataReader;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
using Microsoft.JSInterop;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Data_Coupler.Services;
|
||||
using Data_Coupler.Models;
|
||||
|
||||
namespace Data_Coupler.Pages;
|
||||
|
||||
public partial class DataCoupler : ComponentBase
|
||||
{
|
||||
// ===== PROPRIETÀ DATABASE =====
|
||||
|
||||
// Stato delle credenziali database
|
||||
protected List<DatabaseCredential> databaseCredentials = new();
|
||||
protected string selectedDatabaseCredential = "";
|
||||
|
||||
// Stato connessioni database
|
||||
protected bool isConnectingDatabase = false;
|
||||
protected bool isDatabaseConnected = false;
|
||||
protected string databaseErrorMessage = "";
|
||||
|
||||
// Database discovery
|
||||
protected List<string> availableTableNames = new(); // Solo nomi delle tabelle
|
||||
protected Dictionary<string, IEnumerable<DbColumnInfo>> databaseTables = new(); // Schema dettagliato per tabelle caricate
|
||||
protected string selectedTable = "";
|
||||
protected string databaseSearchTerm = "";
|
||||
|
||||
// Database selection - per gestire la selezione del database quando non specificato nella connection string
|
||||
protected List<string> availableDatabases = new();
|
||||
protected string selectedDatabase = "";
|
||||
protected bool showDatabaseSelectionModal = false;
|
||||
protected bool isLoadingDatabases = false;
|
||||
|
||||
// Database selection (schemas only)
|
||||
protected List<string> availableSchemas = new();
|
||||
protected string selectedSchema = "";
|
||||
protected bool showSchemaSelectionModal = false;
|
||||
protected bool isLoadingSchemas = false;
|
||||
|
||||
// Custom query functionality
|
||||
protected bool useCustomQuery = false;
|
||||
protected string customQuery = "";
|
||||
protected bool isValidatingQuery = false;
|
||||
protected bool isQueryValid = false;
|
||||
protected string queryValidationMessage = "";
|
||||
protected List<Dictionary<string, object>> queryPreviewData = new();
|
||||
protected List<string> queryColumns = new();
|
||||
protected bool showQueryPreview = false;
|
||||
protected bool isLoadingPreview = false;
|
||||
|
||||
// Gestione chiavi sorgente per database
|
||||
protected string suggestedPrimaryKey = ""; // Campo PK suggerito per database
|
||||
|
||||
// Servizi database
|
||||
protected IDatabaseManager? currentDatabaseManager = null;
|
||||
|
||||
// ===== METODI DATABASE =====
|
||||
|
||||
/// <summary>
|
||||
/// Gestisce il cambio di credenziale database selezionata
|
||||
/// </summary>
|
||||
protected void OnDatabaseCredentialChanged(ChangeEventArgs e)
|
||||
{
|
||||
selectedDatabaseCredential = e.Value?.ToString() ?? "";
|
||||
ResetDatabaseState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resetta lo stato del database
|
||||
/// </summary>
|
||||
protected void ResetDatabaseState()
|
||||
{
|
||||
isDatabaseConnected = false;
|
||||
databaseTables.Clear();
|
||||
selectedTable = "";
|
||||
databaseSearchTerm = "";
|
||||
databaseErrorMessage = "";
|
||||
|
||||
// Reset database selection
|
||||
availableDatabases.Clear();
|
||||
selectedDatabase = "";
|
||||
showDatabaseSelectionModal = false;
|
||||
isLoadingDatabases = false;
|
||||
|
||||
// Reset custom query state
|
||||
useCustomQuery = false;
|
||||
customQuery = "";
|
||||
isValidatingQuery = false;
|
||||
isQueryValid = false;
|
||||
queryValidationMessage = "";
|
||||
queryPreviewData.Clear();
|
||||
queryColumns.Clear();
|
||||
showQueryPreview = false;
|
||||
isLoadingPreview = false;
|
||||
|
||||
currentDatabaseManager?.Dispose();
|
||||
currentDatabaseManager = null;
|
||||
|
||||
// Clear mappings when resetting database state
|
||||
ClearAllMappings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connette al database utilizzando le credenziali selezionate
|
||||
/// </summary>
|
||||
protected 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
|
||||
Logger.LogInformation("Creando database manager per credenziale: {CredentialName}", selectedDatabaseCredential);
|
||||
currentDatabaseManager = await ConnectionFactory.CreateDatabaseManagerAsync(selectedDatabaseCredential);
|
||||
Logger.LogInformation("Database manager creato con successo");
|
||||
|
||||
// Verifica se il database è specificato nella connection string
|
||||
bool isDatabaseSpecified = await IsDatabaseSpecifiedInConnectionString(credential);
|
||||
|
||||
if (isDatabaseSpecified)
|
||||
{
|
||||
Logger.LogInformation("Database specificato nella connection string. Procedendo con discovery tabelle.");
|
||||
try
|
||||
{
|
||||
await LoadTablesFromConnectedDatabase();
|
||||
isDatabaseConnected = true;
|
||||
Logger.LogInformation("Tabelle caricate con successo, database connesso");
|
||||
return; // Importante: usciamo qui se tutto va bene
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Errore nel caricamento tabelle dal database specificato");
|
||||
databaseErrorMessage = $"Errore nel caricamento tabelle: {ex.Message}";
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInformation("Database non specificato nella connection string. Caricando database disponibili.");
|
||||
await LoadAvailableDatabases();
|
||||
|
||||
if (availableDatabases.Any())
|
||||
{
|
||||
Logger.LogInformation("Trovati {DatabaseCount} database disponibili", availableDatabases.Count);
|
||||
showDatabaseSelectionModal = true;
|
||||
StateHasChanged();
|
||||
return; // Non procediamo fino alla selezione del database
|
||||
}
|
||||
else
|
||||
{
|
||||
databaseErrorMessage = "Nessun database disponibile trovato";
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Errore nella connessione al database");
|
||||
databaseErrorMessage = $"Errore: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isConnectingDatabase = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connette a un database specifico
|
||||
/// </summary>
|
||||
protected async Task ConnectToDatabaseWithSpecificDatabase(string databaseName)
|
||||
{
|
||||
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
|
||||
Logger.LogInformation("Creando database manager per credenziale: {CredentialName}", selectedDatabaseCredential);
|
||||
currentDatabaseManager = await ConnectionFactory.CreateDatabaseManagerAsync(selectedDatabaseCredential);
|
||||
Logger.LogInformation("Database manager creato con successo");
|
||||
if (currentDatabaseManager == null)
|
||||
{
|
||||
databaseErrorMessage = "Database manager non disponibile";
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogInformation("Cambiando database a: {DatabaseName}", databaseName);
|
||||
|
||||
// Usa il metodo dell'interfaccia per cambiare database
|
||||
await currentDatabaseManager.ChangeDatabaseAsync(databaseName);
|
||||
Logger.LogInformation("Database cambiato con successo a: {DatabaseName}", databaseName);
|
||||
|
||||
// Carica le tabelle dal database selezionato
|
||||
await LoadTablesFromConnectedDatabase();
|
||||
|
||||
isDatabaseConnected = true;
|
||||
Logger.LogInformation("Connessione completata per database: {DatabaseName}", databaseName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Errore nella connessione al database specifico: {DatabaseName}", databaseName);
|
||||
databaseErrorMessage = $"Errore nella connessione al database {databaseName}: {ex.Message}";
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
isConnectingDatabase = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seleziona una tabella database e carica il suo schema
|
||||
/// </summary>
|
||||
protected async Task SelectTable(string tableName)
|
||||
{
|
||||
selectedTable = tableName;
|
||||
|
||||
// Clear custom query state when selecting a table
|
||||
useCustomQuery = false;
|
||||
customQuery = "";
|
||||
isQueryValid = false;
|
||||
queryValidationMessage = "";
|
||||
queryPreviewData.Clear();
|
||||
queryColumns.Clear();
|
||||
showQueryPreview = false;
|
||||
|
||||
// Clear mappings when changing table
|
||||
ClearAllMappings();
|
||||
|
||||
// Reset key field logic
|
||||
sourceKeyField = "";
|
||||
suggestedPrimaryKey = "";
|
||||
requiresManualKeySelection = false;
|
||||
|
||||
// Carica i dettagli della tabella se non sono già stati caricati
|
||||
if (!databaseTables.ContainsKey(tableName) && currentDatabaseManager != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tableSchema = await currentDatabaseManager.GetTableSchemaAsync(tableName);
|
||||
databaseTables[tableName] = tableSchema;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Errore nel caricamento dello schema della tabella {TableName}", tableName);
|
||||
databaseErrorMessage = $"Errore nel caricamento della tabella: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
// If it's a database source, try to detect the primary key
|
||||
if (selectedSourceType == "database" && currentDatabaseManager != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var primaryKey = await currentDatabaseManager.GetPrimaryKeyFieldAsync(tableName);
|
||||
if (!string.IsNullOrEmpty(primaryKey))
|
||||
{
|
||||
suggestedPrimaryKey = primaryKey;
|
||||
|
||||
// AUTO-SELECT: Imposta automaticamente il campo chiave se rilevato
|
||||
sourceKeyField = primaryKey;
|
||||
requiresManualKeySelection = false;
|
||||
|
||||
Logger.LogInformation("Chiave primaria rilevata e auto-selezionata per la tabella {TableName}: {PrimaryKey}", tableName, primaryKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No primary key found, require manual selection
|
||||
requiresManualKeySelection = true;
|
||||
sourceKeyField = "";
|
||||
Logger.LogInformation("Nessuna chiave primaria trovata per la tabella {TableName}, selezione manuale richiesta", tableName);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Errore nel rilevamento della chiave primaria per la tabella {TableName}", tableName);
|
||||
requiresManualKeySelection = true;
|
||||
sourceKeyField = "";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// For non-database sources, always require manual selection
|
||||
requiresManualKeySelection = true;
|
||||
sourceKeyField = "";
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene le tabelle filtrate in base al termine di ricerca
|
||||
/// </summary>
|
||||
protected IEnumerable<string> GetFilteredDatabaseTables()
|
||||
{
|
||||
if (string.IsNullOrEmpty(databaseSearchTerm))
|
||||
return availableTableNames;
|
||||
|
||||
return availableTableNames.Where(table =>
|
||||
table.Contains(databaseSearchTerm, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filtra le tabelle database in base al termine di ricerca
|
||||
/// </summary>
|
||||
protected async Task FilterDatabaseTables(ChangeEventArgs e)
|
||||
{
|
||||
databaseSearchTerm = e.Value?.ToString() ?? "";
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pulisce il filtro di ricerca delle tabelle database
|
||||
/// </summary>
|
||||
protected async Task ClearDatabaseSearch()
|
||||
{
|
||||
databaseSearchTerm = "";
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seleziona una colonna database per il mapping
|
||||
/// </summary>
|
||||
protected void SelectDbColumn(string columnName)
|
||||
{
|
||||
selectedDbColumn = columnName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifica se il database è specificato nella connection string
|
||||
/// </summary>
|
||||
protected Task<bool> IsDatabaseSpecifiedInConnectionString(DatabaseCredential credential)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInformation("Verifica database specificato - Tipo: {DatabaseType}, DatabaseName: '{DatabaseName}', Connection: {ConnectionString}",
|
||||
credential.DatabaseType, credential.DatabaseName, credential.ConnectionString?.Substring(0, Math.Min(100, credential.ConnectionString?.Length ?? 0)));
|
||||
|
||||
// Prima verifica se c'è un database specificato nel campo DatabaseName della credenziale
|
||||
if (!string.IsNullOrEmpty(credential.DatabaseName))
|
||||
{
|
||||
Logger.LogInformation("Database specificato nel campo DatabaseName: '{DatabaseName}' - RESULT: TRUE", credential.DatabaseName);
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
// Per SQL Server verifica se Initial Catalog o Database è specificato nella connection string
|
||||
if (credential.DatabaseType == DatabaseType.SqlServer)
|
||||
{
|
||||
var connectionString = credential.ConnectionString ?? "";
|
||||
var hasInitialCatalog = connectionString.Contains("Initial Catalog=", StringComparison.OrdinalIgnoreCase);
|
||||
var hasDatabase = connectionString.Contains("Database=", StringComparison.OrdinalIgnoreCase);
|
||||
var result = hasInitialCatalog || hasDatabase;
|
||||
|
||||
Logger.LogInformation("SQL Server - HasInitialCatalog: {HasInitialCatalog}, HasDatabase: {HasDatabase}, Result: {Result}",
|
||||
hasInitialCatalog, hasDatabase, result);
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
// TODO: Implementare per altri tipi di database
|
||||
// MySQL: Database=
|
||||
// PostgreSQL: Database=
|
||||
// Oracle: più complesso con SID/Service Name
|
||||
|
||||
Logger.LogWarning("Verifica database specificato non implementata per tipo database: {DatabaseType}", credential.DatabaseType);
|
||||
return Task.FromResult(true); // Default: assume database specificato per tipi non implementati
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Errore nella verifica database specificato in connection string");
|
||||
return Task.FromResult(true); // Default: assume database specificato in caso di errore
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Carica le tabelle dal database attualmente connesso
|
||||
/// </summary>
|
||||
protected async Task LoadTablesFromConnectedDatabase()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (currentDatabaseManager == null)
|
||||
{
|
||||
databaseErrorMessage = "Database manager non disponibile";
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogInformation("Caricando tabelle dal database connesso");
|
||||
var tableNames = await currentDatabaseManager.GetTableNamesAsync();
|
||||
availableTableNames = tableNames.ToList();
|
||||
|
||||
Logger.LogInformation("Caricate {Count} tabelle dal database", availableTableNames.Count);
|
||||
|
||||
// Resetta i dettagli delle tabelle - verranno caricati solo quando selezionati
|
||||
databaseTables.Clear();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Errore nel caricamento delle tabelle dal database connesso");
|
||||
databaseErrorMessage = $"Errore nel caricamento tabelle: {ex.Message}";
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Carica la lista dei database disponibili dal server
|
||||
/// </summary>
|
||||
protected async Task LoadAvailableDatabases()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (currentDatabaseManager == null)
|
||||
{
|
||||
databaseErrorMessage = "Database manager non disponibile";
|
||||
return;
|
||||
}
|
||||
|
||||
isLoadingDatabases = true;
|
||||
Logger.LogInformation("Caricando database disponibili");
|
||||
|
||||
// Usa il metodo corretto dell'interfaccia IDatabaseManager
|
||||
var allDatabases = await currentDatabaseManager.GetAvailableDatabasesAsync();
|
||||
Logger.LogInformation("Ottenuti {DatabaseCount} database dal server", allDatabases.Count);
|
||||
|
||||
// Filtra i database di sistema
|
||||
availableDatabases = FilterSystemDatabases(allDatabases).ToList();
|
||||
|
||||
Logger.LogInformation("Trovati {TotalDatabases} database, filtrati a {FilteredDatabases} (esclusi quelli di sistema)",
|
||||
allDatabases.Count, availableDatabases.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Errore nel caricamento dei database disponibili");
|
||||
databaseErrorMessage = $"Errore nel caricamento database: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoadingDatabases = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tenta di auto-selezionare una chiave per le query custom
|
||||
/// </summary>
|
||||
protected void TryAutoSelectKeyForQuery(List<string> columns)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Reset stato chiave
|
||||
sourceKeyField = "";
|
||||
suggestedPrimaryKey = "";
|
||||
requiresManualKeySelection = true;
|
||||
|
||||
// Pattern comuni per identificare possibili chiavi primarie
|
||||
var keyPatterns = new[]
|
||||
{
|
||||
"id", "ID", "Id",
|
||||
"_id", "_ID", "_Id",
|
||||
"key", "KEY", "Key",
|
||||
"code", "CODE", "Code", "codice", "CODICE", "Codice",
|
||||
"number", "NUMBER", "Number", "numero", "NUMERO", "Numero",
|
||||
"index", "INDEX", "Index", "indice", "INDICE", "Indice"
|
||||
};
|
||||
|
||||
// Cerca colonne che potrebbero essere chiavi primarie
|
||||
string? detectedKey = null;
|
||||
|
||||
// 1. Cerca esattamente "id", "ID", "Id"
|
||||
detectedKey = columns.FirstOrDefault(c =>
|
||||
c.Equals("id", StringComparison.OrdinalIgnoreCase) ||
|
||||
c.Equals("ID", StringComparison.Ordinal) ||
|
||||
c.Equals("Id", StringComparison.Ordinal));
|
||||
|
||||
// 2. Se non trovato, cerca colonne che terminano con "id", "ID", "Id"
|
||||
if (detectedKey == null)
|
||||
{
|
||||
detectedKey = columns.FirstOrDefault(c =>
|
||||
c.EndsWith("id", StringComparison.OrdinalIgnoreCase) ||
|
||||
c.EndsWith("ID", StringComparison.Ordinal) ||
|
||||
c.EndsWith("Id", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
// 3. Se non trovato, cerca colonne che contengono pattern di chiave
|
||||
if (detectedKey == null)
|
||||
{
|
||||
foreach (var pattern in keyPatterns)
|
||||
{
|
||||
detectedKey = columns.FirstOrDefault(c =>
|
||||
c.Contains(pattern, StringComparison.OrdinalIgnoreCase));
|
||||
if (detectedKey != null) break;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Auto-seleziona se trovato
|
||||
if (!string.IsNullOrEmpty(detectedKey))
|
||||
{
|
||||
sourceKeyField = detectedKey;
|
||||
suggestedPrimaryKey = detectedKey;
|
||||
requiresManualKeySelection = false;
|
||||
|
||||
Logger.LogInformation("Chiave auto-selezionata per query custom: {KeyField}", detectedKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInformation("Nessuna chiave rilevabile automaticamente per query custom, selezione manuale richiesta");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Errore nell'auto-selezione della chiave per query custom");
|
||||
sourceKeyField = "";
|
||||
suggestedPrimaryKey = "";
|
||||
requiresManualKeySelection = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Valida una custom query SQL
|
||||
/// </summary>
|
||||
protected async Task ValidateCustomQuery()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(customQuery) || currentDatabaseManager == null)
|
||||
{
|
||||
isQueryValid = false;
|
||||
queryValidationMessage = "Query vuota o manager database non disponibile";
|
||||
return;
|
||||
}
|
||||
|
||||
isValidatingQuery = true;
|
||||
|
||||
try
|
||||
{
|
||||
// Controllo di sicurezza: verifica che sia una SELECT
|
||||
if (!IsSelectQuery(customQuery))
|
||||
{
|
||||
isQueryValid = false;
|
||||
queryValidationMessage = "Solo query SELECT sono permesse per sicurezza";
|
||||
return;
|
||||
}
|
||||
|
||||
var cleanQuery = CleanQuery(customQuery);
|
||||
|
||||
// Trova la credenziale per determinare il tipo di database
|
||||
var credential = databaseCredentials.FirstOrDefault(c => c.Name == selectedDatabaseCredential);
|
||||
if (credential == null)
|
||||
{
|
||||
isQueryValid = false;
|
||||
queryValidationMessage = "Credenziale database non trovata";
|
||||
return;
|
||||
}
|
||||
|
||||
// Crea una query di test con sintassi appropriata per il tipo di database
|
||||
var testQuery = CreateLimitedQuery(cleanQuery, credential.DatabaseType, 1);
|
||||
|
||||
Logger.LogInformation("Validando query: {Query}", testQuery);
|
||||
|
||||
// Prova a eseguire la query per validarla
|
||||
var testResults = await currentDatabaseManager.ExecuteRawQueryAsync(testQuery);
|
||||
|
||||
if (testResults != null && testResults.Any())
|
||||
{
|
||||
var firstRow = testResults.First();
|
||||
queryColumns = firstRow.Keys.ToList();
|
||||
isQueryValid = true;
|
||||
queryValidationMessage = $"Query valida - {queryColumns.Count} colonne rilevate";
|
||||
|
||||
// Clear mappings quando cambia la query
|
||||
ClearAllMappings();
|
||||
|
||||
// AUTO-SELECT della chiave per query custom
|
||||
TryAutoSelectKeyForQuery(queryColumns);
|
||||
|
||||
Logger.LogInformation("Query validata con successo: {ColumnCount} colonne", queryColumns.Count);
|
||||
}
|
||||
else
|
||||
{
|
||||
isQueryValid = false;
|
||||
queryValidationMessage = "La query non ha restituito risultati o colonne";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Errore nella validazione della query");
|
||||
isQueryValid = false;
|
||||
queryValidationMessage = $"Errore nella validazione: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isValidatingQuery = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Carica un'anteprima dei dati della query
|
||||
/// </summary>
|
||||
protected async Task LoadQueryPreview()
|
||||
{
|
||||
if (!isQueryValid || string.IsNullOrWhiteSpace(customQuery) || currentDatabaseManager == null)
|
||||
return;
|
||||
|
||||
isLoadingPreview = true;
|
||||
|
||||
try
|
||||
{
|
||||
var cleanQuery = CleanQuery(customQuery);
|
||||
|
||||
// Trova la credenziale per determinare il tipo di database
|
||||
var credential = databaseCredentials.FirstOrDefault(c => c.Name == selectedDatabaseCredential);
|
||||
if (credential == null)
|
||||
{
|
||||
queryValidationMessage = "Credenziale database non trovata";
|
||||
return;
|
||||
}
|
||||
|
||||
// Crea una query di anteprima con sintassi appropriata per il tipo di database
|
||||
var previewQuery = CreateLimitedQuery(cleanQuery, credential.DatabaseType, 10);
|
||||
|
||||
Logger.LogInformation("Caricando anteprima con query: {Query}", previewQuery);
|
||||
|
||||
var previewResults = await currentDatabaseManager.ExecuteRawQueryAsync(previewQuery);
|
||||
queryPreviewData = previewResults.ToList();
|
||||
showQueryPreview = true;
|
||||
|
||||
Logger.LogInformation("Caricata anteprima query con {RecordCount} record", queryPreviewData.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Errore nel caricamento anteprima query");
|
||||
queryValidationMessage = $"Errore anteprima: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoadingPreview = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Crea una query limitata in base al tipo di database
|
||||
/// </summary>
|
||||
protected string CreateLimitedQuery(string baseQuery, DatabaseType databaseType, int limit)
|
||||
{
|
||||
return databaseType switch
|
||||
{
|
||||
DatabaseType.SqlServer => $"SELECT TOP {limit} * FROM ({baseQuery}) AS subquery",
|
||||
DatabaseType.Oracle => $"SELECT * FROM ({baseQuery}) WHERE ROWNUM <= {limit}",
|
||||
DatabaseType.MySql => $"{baseQuery} LIMIT {limit}",
|
||||
DatabaseType.PostgreSql => $"{baseQuery} LIMIT {limit}",
|
||||
DatabaseType.Sqlite => $"{baseQuery} LIMIT {limit}",
|
||||
DatabaseType.DB2 => $"SELECT * FROM ({baseQuery}) FETCH FIRST {limit} ROWS ONLY",
|
||||
DatabaseType.SapHana => $"{baseQuery} LIMIT {limit}",
|
||||
_ => $"{baseQuery} LIMIT {limit}" // Default a LIMIT per database non riconosciuti
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Nasconde l'anteprima della query
|
||||
/// </summary>
|
||||
protected void HideQueryPreview()
|
||||
{
|
||||
showQueryPreview = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tenta di caricare gli schemi con query diretta
|
||||
/// </summary>
|
||||
protected async Task TryLoadSchemasWithDirectQuery()
|
||||
{
|
||||
if (currentDatabaseManager == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
// Query diverse per ogni tipo di database - focalizzate sui database/cataloghi
|
||||
var credential = databaseCredentials.FirstOrDefault(c => c.Name == selectedDatabaseCredential);
|
||||
if (credential == null) return;
|
||||
|
||||
string? schemaQuery = credential.DatabaseType switch
|
||||
{
|
||||
DatabaseType.SqlServer => "SELECT name FROM sys.databases WHERE name NOT IN ('master', 'tempdb', 'model', 'msdb') AND state = 0",
|
||||
DatabaseType.PostgreSql => "SELECT datname FROM pg_database WHERE datistemplate = false AND datname NOT IN ('postgres', 'template0', 'template1')",
|
||||
DatabaseType.MySql => "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys')",
|
||||
DatabaseType.Oracle => "SELECT DISTINCT OWNER FROM ALL_TABLES WHERE OWNER NOT IN ('SYS', 'SYSTEM', 'DBSNMP', 'SYSMAN', 'OUTLN', 'ANONYMOUS', 'CTXSYS', 'EXFSYS', 'LBACSYS', 'MDSYS', 'MGMT_VIEW', 'OLAPSYS', 'OWBSYS', 'ORDDATA', 'ORDSYS', 'SI_INFORMTN_SCHEMA', 'WK_TEST', 'WKPROXY', 'WMSYS', 'XDB', 'APEX_040000', 'APEX_PUBLIC_USER', 'DIP', 'FLOWS_FILES', 'HR', 'IX', 'OE', 'PM', 'SCOTT', 'SH', 'BI')",
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(schemaQuery))
|
||||
{
|
||||
Logger.LogInformation("Eseguendo query per database/schemi: {Query}", schemaQuery);
|
||||
var results = await currentDatabaseManager.ExecuteRawQueryAsync(schemaQuery);
|
||||
|
||||
if (results != null && results.Any())
|
||||
{
|
||||
var schemas = results.Select(row =>
|
||||
{
|
||||
var firstValue = row.Values.FirstOrDefault();
|
||||
return firstValue?.ToString() ?? "";
|
||||
})
|
||||
.Where(schema => !string.IsNullOrEmpty(schema))
|
||||
.OrderBy(schema => schema)
|
||||
.ToList();
|
||||
|
||||
if (schemas.Any())
|
||||
{
|
||||
availableSchemas.AddRange(schemas);
|
||||
Logger.LogInformation("Caricati {SchemaCount} database/schemi via query diretta per {DatabaseType}: {Schemas}",
|
||||
schemas.Count, credential.DatabaseType, string.Join(", ", schemas));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Errore nel caricamento database/schemi via query diretta");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gestisce la selezione database quando la discovery restituisce dizionario vuoto
|
||||
/// </summary>
|
||||
protected async Task HandleDatabaseSelectionRequired()
|
||||
{
|
||||
isLoadingDatabases = true;
|
||||
showDatabaseSelectionModal = true;
|
||||
availableDatabases.Clear();
|
||||
selectedDatabase = "";
|
||||
try
|
||||
{
|
||||
if (currentDatabaseManager != null)
|
||||
{
|
||||
var dbs = await currentDatabaseManager.GetAvailableDatabasesAsync();
|
||||
availableDatabases = dbs ?? new List<string>();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
databaseErrorMessage = $"Errore nel caricamento dei database: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoadingDatabases = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gestisce la selezione di un database specifico
|
||||
/// </summary>
|
||||
protected async Task OnDatabaseSelected()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(selectedDatabase))
|
||||
{
|
||||
databaseErrorMessage = "Nessun database selezionato";
|
||||
return;
|
||||
}
|
||||
|
||||
showDatabaseSelectionModal = false;
|
||||
|
||||
Logger.LogInformation("Database selezionato: {DatabaseName}. Riconnessione in corso...", selectedDatabase);
|
||||
|
||||
// Riconnessione al database selezionato
|
||||
await ConnectToDatabaseWithSpecificDatabase(selectedDatabase);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Errore nella selezione del database: {DatabaseName}", selectedDatabase);
|
||||
databaseErrorMessage = $"Errore nella connessione al database {selectedDatabase}: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<string> FilterSystemDatabases(List<string> allDatabases)
|
||||
{
|
||||
// Trova la credenziale per determinare il tipo di database
|
||||
var credential = databaseCredentials.FirstOrDefault(c => c.Name == selectedDatabaseCredential);
|
||||
if (credential == null)
|
||||
{
|
||||
Logger.LogWarning("Credenziale non trovata per filtraggio database di sistema");
|
||||
return allDatabases; // Restituisce tutti se non riesce a determinare il tipo
|
||||
}
|
||||
|
||||
var databaseType = credential.DatabaseType;
|
||||
|
||||
// Filtri per SQL Server
|
||||
if (databaseType == DatabaseType.SqlServer)
|
||||
{
|
||||
var sqlServerSystemDatabases = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"master", "tempdb", "model", "msdb", "Resource", "mssqlsystemresource",
|
||||
"ReportServer", "ReportServerTempDB", "SSISDB", "distribution"
|
||||
};
|
||||
|
||||
return allDatabases.Where(db => !sqlServerSystemDatabases.Contains(db));
|
||||
}
|
||||
|
||||
// TODO: Implementare filtri per altri tipi di database
|
||||
if (databaseType == DatabaseType.MySql)
|
||||
{
|
||||
Logger.LogInformation("Filtro database di sistema MySQL - DA IMPLEMENTARE");
|
||||
return allDatabases; // Per ora restituisce tutti
|
||||
}
|
||||
|
||||
if (databaseType == DatabaseType.PostgreSql)
|
||||
{
|
||||
Logger.LogInformation("Filtro database di sistema PostgreSQL - DA IMPLEMENTARE");
|
||||
return allDatabases; // Per ora restituisce tutti
|
||||
}
|
||||
|
||||
if (databaseType == DatabaseType.Oracle)
|
||||
{
|
||||
Logger.LogInformation("Filtro database di sistema Oracle - DA IMPLEMENTARE");
|
||||
return allDatabases; // Per ora restituisce tutti
|
||||
}
|
||||
|
||||
Logger.LogWarning("Tipo database non riconosciuto per filtraggio: {DatabaseType}", databaseType);
|
||||
return allDatabases; // Restituisce tutti per tipi non riconosciuti
|
||||
}
|
||||
|
||||
private void CancelDatabaseSelection()
|
||||
{
|
||||
showDatabaseSelectionModal = false;
|
||||
selectedDatabase = "";
|
||||
databaseErrorMessage = "Selezione database annullata";
|
||||
Logger.LogInformation("Selezione database annullata dall'utente");
|
||||
}
|
||||
|
||||
// ===== FINE METODI DATABASE =====
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
using Microsoft.JSInterop;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using CredentialManager.Models;
|
||||
using DataConnection.REST.Interfaces;
|
||||
using DataConnection.REST.Models;
|
||||
using DataConnection.CredentialManagement.Interfaces;
|
||||
using DataConnection.Interfaces;
|
||||
using Data_Coupler.Services;
|
||||
using Data_Coupler.Models;
|
||||
|
||||
namespace Data_Coupler.Pages;
|
||||
|
||||
public partial class DataCoupler : ComponentBase
|
||||
{
|
||||
// ===== PROPRIETÀ REST =====
|
||||
|
||||
// Credenziali REST
|
||||
protected List<RestApiCredential> restApiCredentials = new();
|
||||
protected string selectedRestCredential = "";
|
||||
|
||||
// Stato connessioni REST
|
||||
protected bool isConnectingRest = false;
|
||||
protected bool isRestConnected = false;
|
||||
protected string restErrorMessage = "";
|
||||
|
||||
// REST discovery
|
||||
protected List<RestEntitySummary> restEntities = new();
|
||||
protected RestEntitySummary? selectedRestEntity = null;
|
||||
protected RestEntityInfo? restEntityDetails = null;
|
||||
protected string restSearchTerm = "";
|
||||
|
||||
// Proprietà di mapping REST
|
||||
protected string selectedRestProperty = "";
|
||||
|
||||
// Servizi REST
|
||||
protected IRestMetadataDiscovery? currentRestDiscovery = null;
|
||||
protected IRestServiceClient? currentRestClient = null;
|
||||
|
||||
// ===== METODI REST =====
|
||||
|
||||
/// <summary>
|
||||
/// Carica le credenziali REST API
|
||||
/// </summary>
|
||||
protected async Task LoadRestCredentials()
|
||||
{
|
||||
try
|
||||
{
|
||||
restApiCredentials = await CredentialService.GetAllRestApiCredentialsAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Errore nel caricamento delle credenziali REST");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gestisce il cambio di credenziale REST
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resetta lo stato REST
|
||||
/// </summary>
|
||||
protected void ResetRestState()
|
||||
{
|
||||
isRestConnected = false;
|
||||
restEntities.Clear();
|
||||
selectedRestEntity = null;
|
||||
restEntityDetails = null;
|
||||
restSearchTerm = "";
|
||||
restErrorMessage = "";
|
||||
currentRestDiscovery = null;
|
||||
currentRestClient = null;
|
||||
|
||||
// Clear mappings when resetting REST state - handled by main class
|
||||
// ClearAllMappings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connette al servizio REST API
|
||||
/// </summary>
|
||||
protected 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 con successo per il servizio REST {ServiceType}", credential.ServiceType);
|
||||
|
||||
// Discovery delle entità disponibili
|
||||
Logger.LogInformation("Iniziando discovery delle entità REST...");
|
||||
var entities = await currentRestDiscovery.DiscoverEntitiesAsync();
|
||||
|
||||
restEntities = entities.Select(e => new RestEntitySummary
|
||||
{
|
||||
Name = e.Name,
|
||||
Label = e.Name, // Use Name as Label since RestEntityInfo doesn't have Label
|
||||
Description = "" // RestEntityInfo doesn't have Description
|
||||
}).ToList();
|
||||
isRestConnected = true;
|
||||
|
||||
Logger.LogInformation("Discovery completato: trovate {EntityCount} entità REST", restEntities.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Errore nella connessione al servizio REST {ServiceType}", selectedRestCredential);
|
||||
restErrorMessage = $"Errore nella connessione: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isConnectingRest = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seleziona un'entità REST
|
||||
/// </summary>
|
||||
protected async Task SelectRestEntity(RestEntitySummary entity)
|
||||
{
|
||||
selectedRestEntity = entity;
|
||||
|
||||
// Clear mappings when changing entity - handled by main class
|
||||
// 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}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filtra le entità REST in base al termine di ricerca
|
||||
/// </summary>
|
||||
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)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applica il filtro alle entità REST
|
||||
/// </summary>
|
||||
private async Task FilterRestEntities(ChangeEventArgs e)
|
||||
{
|
||||
restSearchTerm = e.Value?.ToString() ?? "";
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pulisce la ricerca delle entità REST
|
||||
/// </summary>
|
||||
private async Task ClearRestSearch()
|
||||
{
|
||||
restSearchTerm = "";
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seleziona una proprietà REST per il mapping
|
||||
/// </summary>
|
||||
protected void SelectRestProperty(string propertyName)
|
||||
{
|
||||
selectedRestProperty = propertyName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
namespace Data_Coupler.Models;
|
||||
|
||||
// Classe per i risultati del trasferimento
|
||||
public class TransferResult
|
||||
{
|
||||
public int RecordNumber { get; set; }
|
||||
public string Status { get; set; } = ""; // "success", "error", "updated", "duplicate"
|
||||
public string Message { get; set; } = "";
|
||||
public string? EntityId { get; set; }
|
||||
public Dictionary<string, object> RecordData { get; set; } = new();
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user