feat: Implementato supporto per query custom nei profili Data Coupler

- Aggiunto campo SourceCustomQuery al modello DataCouplerProfile e DTO
- Creata migrazione database per la nuova colonna SourceCustomQuery
- Aggiornato DataCouplerProfileService per gestire il mapping della query custom
- Modificato ProfileSaver per includere la query custom nel salvataggio
- Implementata logica di caricamento profili con supporto query custom:
  * Popolamento automatico della textbox con query salvata
  * Validazione ed esecuzione automatica della query al caricamento
  * Caricamento anteprima dati e mapping dopo validazione query
  * Gestione priorità: query custom ha precedenza sulla selezione tabella
- Aggiornato DataCoupler.razor per passare la query custom al ProfileSaver
- Corretto salvataggio profili esistenti per includere SourceCustomQuery

Il sistema ora permette di salvare e ripristinare completamente le configurazioni
con query SQL personalizzate, mantenendo il comportamento esistente per le
This commit is contained in:
2025-07-05 21:56:13 +02:00
parent 7d2961702c
commit ee3c251b08
13 changed files with 419 additions and 4 deletions
+2
View File
@@ -16,6 +16,7 @@ public partial class ProfileSaver
[Parameter] public string? SourceDatabaseName { get; set; }
[Parameter] public string? SourceSchema { get; set; }
[Parameter] public string? SourceTable { get; set; }
[Parameter] public string? SourceCustomQuery { get; set; }
[Parameter] public string? SourceFilePath { get; set; }
[Parameter] public string DestinationType { get; set; } = "";
[Parameter] public int? DestinationCredentialId { get; set; }
@@ -68,6 +69,7 @@ public partial class ProfileSaver
SourceDatabaseName = sourceDatabaseName,
SourceSchema = SourceSchema,
SourceTable = SourceTable,
SourceCustomQuery = SourceCustomQuery,
SourceFilePath = SourceFilePath,
DestinationType = DestinationType,
DestinationCredentialId = DestinationCredentialId,
@@ -0,0 +1,341 @@
// <auto-generated />
using System;
using CredentialManager.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace CredentialManager.Migrations
{
[DbContext(typeof(CredentialDbContext))]
[Migration("20250705194127_AddSourceCustomQueryColumn")]
partial class AddSourceCustomQueryColumn
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.0");
modelBuilder.Entity("CredentialManager.Models.CredentialEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AdditionalParameters")
.HasMaxLength(2000)
.HasColumnType("TEXT");
b.Property<int>("CommandTimeout")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(30);
b.Property<string>("ConnectionString")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("CreatedBy")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("DatabaseName")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("DatabaseType")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("EncryptedApiKey")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<string>("EncryptedAuthToken")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<string>("EncryptedPassword")
.HasColumnType("TEXT");
b.Property<string>("Headers")
.HasMaxLength(2000)
.HasColumnType("TEXT");
b.Property<string>("Host")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<bool>("IgnoreSslErrors")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false);
b.Property<bool>("IsActive")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(true);
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<int?>("Port")
.HasColumnType("INTEGER");
b.Property<string>("RestServiceType")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<int>("TimeoutSeconds")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(100);
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("Username")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("DatabaseType");
b.HasIndex("IsActive");
b.HasIndex("Name")
.IsUnique();
b.HasIndex("Type");
b.ToTable("Credentials", (string)null);
});
modelBuilder.Entity("CredentialManager.Models.DataCouplerProfile", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValueSql("CURRENT_TIMESTAMP");
b.Property<string>("CreatedBy")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<int?>("DestinationCredentialId")
.HasColumnType("INTEGER");
b.Property<string>("DestinationEndpoint")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<string>("DestinationSchema")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("DestinationTable")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("DestinationType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT");
b.Property<string>("FieldMappingJson")
.HasMaxLength(4000)
.HasColumnType("TEXT");
b.Property<bool>("IsActive")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(true);
b.Property<DateTime?>("LastUsedAt")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<int?>("SourceCredentialId")
.HasColumnType("INTEGER");
b.Property<string>("SourceCustomQuery")
.HasMaxLength(2000)
.HasColumnType("TEXT");
b.Property<string>("SourceDatabaseName")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("SourceFilePath")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<string>("SourceKeyField")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("SourceSchema")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("SourceTable")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("SourceType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT");
b.Property<bool>("UseRecordAssociations")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("CreatedAt");
b.HasIndex("DestinationCredentialId");
b.HasIndex("DestinationType");
b.HasIndex("IsActive");
b.HasIndex("LastUsedAt");
b.HasIndex("Name")
.IsUnique();
b.HasIndex("SourceCredentialId");
b.HasIndex("SourceType");
b.ToTable("DataCouplerProfiles", (string)null);
});
modelBuilder.Entity("CredentialManager.Models.KeyAssociation", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AdditionalInfo")
.HasMaxLength(2000)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("DestinationEntity")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("DestinationId")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("DestinationKeyField")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<bool>("IsActive")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(true);
b.Property<string>("KeyValue")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<DateTime?>("LastVerifiedAt")
.HasColumnType("TEXT");
b.Property<string>("RestCredentialName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("SourceKeyField")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("SourcesInfo")
.HasMaxLength(2000)
.HasColumnType("TEXT");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("CreatedAt");
b.HasIndex("DestinationEntity");
b.HasIndex("IsActive");
b.HasIndex("KeyValue")
.HasDatabaseName("IX_KeyAssociations_KeyValue");
b.HasIndex("LastVerifiedAt");
b.HasIndex("RestCredentialName");
b.HasIndex("KeyValue", "DestinationEntity", "RestCredentialName")
.IsUnique()
.HasDatabaseName("IX_KeyAssociations_Unique");
b.ToTable("KeyAssociations", (string)null);
});
modelBuilder.Entity("CredentialManager.Models.DataCouplerProfile", b =>
{
b.HasOne("CredentialManager.Models.CredentialEntity", "DestinationCredential")
.WithMany()
.HasForeignKey("DestinationCredentialId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("CredentialManager.Models.CredentialEntity", "SourceCredential")
.WithMany()
.HasForeignKey("SourceCredentialId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("DestinationCredential");
b.Navigation("SourceCredential");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CredentialManager.Migrations
{
/// <inheritdoc />
public partial class AddSourceCustomQueryColumn : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "SourceCustomQuery",
table: "DataCouplerProfiles",
type: "TEXT",
maxLength: 2000,
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "SourceCustomQuery",
table: "DataCouplerProfiles");
}
}
}
@@ -182,6 +182,10 @@ namespace CredentialManager.Migrations
b.Property<int?>("SourceCredentialId")
.HasColumnType("INTEGER");
b.Property<string>("SourceCustomQuery")
.HasMaxLength(2000)
.HasColumnType("TEXT");
b.Property<string>("SourceDatabaseName")
.HasMaxLength(200)
.HasColumnType("TEXT");
@@ -34,6 +34,9 @@ public class DataCouplerProfile
[MaxLength(200)]
public string? SourceTable { get; set; }
[MaxLength(2000)]
public string? SourceCustomQuery { get; set; }
[MaxLength(500)]
public string? SourceFilePath { get; set; }
@@ -16,6 +16,7 @@ public class DataCouplerProfileDto
public string? SourceDatabaseName { get; set; }
public string? SourceSchema { get; set; }
public string? SourceTable { get; set; }
public string? SourceCustomQuery { get; set; }
public string? SourceFilePath { get; set; }
// Informazioni destinazione
@@ -217,6 +217,7 @@ public class DataCouplerProfileService : IDataCouplerProfileService
SourceDatabaseName = profile.SourceDatabaseName,
SourceSchema = profile.SourceSchema,
SourceTable = profile.SourceTable,
SourceCustomQuery = profile.SourceCustomQuery,
SourceFilePath = profile.SourceFilePath,
DestinationType = profile.DestinationType,
DestinationCredentialId = profile.DestinationCredentialId,
@@ -245,6 +246,7 @@ public class DataCouplerProfileService : IDataCouplerProfileService
SourceDatabaseName = dto.SourceDatabaseName,
SourceSchema = dto.SourceSchema,
SourceTable = dto.SourceTable,
SourceCustomQuery = dto.SourceCustomQuery,
SourceFilePath = dto.SourceFilePath,
DestinationType = dto.DestinationType,
DestinationCredentialId = dto.DestinationCredentialId,
Binary file not shown.
+1
View File
@@ -1008,6 +1008,7 @@
SourceDatabaseName="@selectedDatabase"
SourceSchema="@GetCurrentDatabaseSchema()"
SourceTable="@(useCustomQuery ? "custom_query" : selectedTable)"
SourceCustomQuery="@(useCustomQuery ? customQuery : null)"
SourceFilePath="@selectedFileName"
DestinationType="rest"
DestinationCredentialId="@(GetCurrentDestinationCredentialIdAsync().Result)"
+36 -4
View File
@@ -257,18 +257,48 @@ public partial class DataCoupler : ComponentBase
Logger.LogInformation("Stato dopo connessione database - Connected: {Connected}, Tables: {TableCount}",
isDatabaseConnected, availableTableNames.Count);
// Seleziona la tabella se specificata e se la connessione è riuscita
if (!string.IsNullOrEmpty(profile.SourceTable) && isDatabaseConnected)
// Gestisci la query custom se specificata nel profilo
if (!string.IsNullOrEmpty(profile.SourceCustomQuery) && isDatabaseConnected)
{
Logger.LogInformation("Caricamento query custom dal profilo: {Query}", profile.SourceCustomQuery);
// Imposta la modalità query custom
useCustomQuery = true;
customQuery = profile.SourceCustomQuery;
// Valida ed esegui la query
await ValidateCustomQuery();
if (isQueryValid)
{
Logger.LogInformation("Query custom caricata e validata con successo");
// Carica l'anteprima dei dati
await LoadQueryPreview();
Logger.LogInformation("Anteprima dati della query custom caricata");
}
else
{
Logger.LogWarning("La query custom dal profilo non è valida: {ValidationMessage}", queryValidationMessage);
}
}
// Seleziona la tabella se specificata e se la connessione è riuscita (solo se non c'è una query custom)
else if (!string.IsNullOrEmpty(profile.SourceTable) && isDatabaseConnected)
{
Logger.LogInformation("Selezione tabella: {Table}", profile.SourceTable);
await SelectTable(profile.SourceTable);
Logger.LogInformation("Tabella selezionata: {SelectedTable}, Schema caricato: {SchemaLoaded}",
selectedTable, databaseTables.ContainsKey(profile.SourceTable));
}
else if (string.IsNullOrEmpty(profile.SourceCustomQuery) && string.IsNullOrEmpty(profile.SourceTable))
{
Logger.LogInformation("Nessuna tabella o query custom specificata nel profilo");
}
else
{
Logger.LogWarning("Impossibile selezionare tabella - Table: {Table}, Connected: {Connected}",
profile.SourceTable, isDatabaseConnected);
Logger.LogWarning("Impossibile selezionare tabella o caricare query custom - Table: {Table}, Query: {Query}, Connected: {Connected}",
profile.SourceTable, profile.SourceCustomQuery, isDatabaseConnected);
}
}
else
@@ -454,6 +484,7 @@ public partial class DataCoupler : ComponentBase
existingProfile.SourceCredentialId = profile.SourceCredentialId;
existingProfile.SourceSchema = profile.SourceSchema;
existingProfile.SourceTable = profile.SourceTable;
existingProfile.SourceCustomQuery = profile.SourceCustomQuery;
existingProfile.SourceFilePath = profile.SourceFilePath;
existingProfile.DestinationType = profile.DestinationType;
existingProfile.DestinationCredentialId = profile.DestinationCredentialId;
@@ -486,6 +517,7 @@ public partial class DataCoupler : ComponentBase
existingProfile.SourceCredentialId = profile.SourceCredentialId;
existingProfile.SourceSchema = profile.SourceSchema;
existingProfile.SourceTable = profile.SourceTable;
existingProfile.SourceCustomQuery = profile.SourceCustomQuery;
existingProfile.SourceFilePath = profile.SourceFilePath;
existingProfile.DestinationType = profile.DestinationType;
existingProfile.DestinationCredentialId = profile.DestinationCredentialId;
Binary file not shown.
Binary file not shown.
Binary file not shown.