feat: Implementato sistema di associazioni chiave per prevenire duplicati nel data coupling
BREAKING CHANGE: Rimosso completamente il vecchio sistema RecordAssociation Modifiche principali: - Sostituito RecordAssociation con KeyAssociation basato sui valori delle chiavi - Implementata logica robusta di UPDATE vs INSERT basata su associazioni esistenti - Aggiunta normalizzazione delle chiavi (.Trim()) per consistenza - Implementato fallback nella ricerca associazioni per maggiore affidabilità - Sostituita verifica pre-UPDATE con tentativo diretto più efficiente Componenti modificati: - Nuovo modello: KeyAssociation.cs con campi ottimizzati - Nuovo servizio: KeyAssociationService.cs con metodi completi - Aggiornato: DataCoupler.razor con logica migliorata di gestione associazioni - Aggiornato: CredentialDbContext per gestire solo KeyAssociations - Aggiornati: tutti i servizi di interfaccia per supportare il nuovo sistema - Creata: pagina KeyAssociations.razor per gestione associazioni - Aggiornato: NavMenu.razor con link alla gestione associazioni Miglioramenti tecnici: - Logica di UPDATE più robusta: tenta direttamente l'aggiornamento invece di verificare prima l'esistenza - Gestione errori migliorata con cleanup automatico delle associazioni non valide - Debug logging estensivo per troubleshooting - Fallback nella ricerca associazioni se parametri specifici falliscono - Normalizzazione valori chiave per prevenire problemi di whitespace Risultato: Il sistema ora previene correttamente i duplicati utilizzando le associazioni per decidere se fare INSERT (nuovo record) o UPDATE (record esistente) basandosi sui valori delle chiavi. Database: - Creata migrazione EF per rimuovere RecordAssociations e aggiungere KeyAssociations - Eliminati file e codice legacy non più necessari
This commit is contained in:
@@ -9,7 +9,7 @@ namespace CredentialManager.Data;
|
|||||||
public class CredentialDbContext : DbContext
|
public class CredentialDbContext : DbContext
|
||||||
{
|
{
|
||||||
public DbSet<CredentialEntity> Credentials { get; set; }
|
public DbSet<CredentialEntity> Credentials { get; set; }
|
||||||
public DbSet<RecordAssociation> RecordAssociations { get; set; }
|
public DbSet<KeyAssociation> KeyAssociations { get; set; }
|
||||||
|
|
||||||
public CredentialDbContext(DbContextOptions<CredentialDbContext> options) : base(options)
|
public CredentialDbContext(DbContextOptions<CredentialDbContext> options) : base(options)
|
||||||
{
|
{
|
||||||
@@ -86,24 +86,24 @@ public class CredentialDbContext : DbContext
|
|||||||
entity.HasIndex(e => e.IsActive);
|
entity.HasIndex(e => e.IsActive);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Configurazione della tabella RecordAssociations
|
// Configurazione della tabella KeyAssociations
|
||||||
modelBuilder.Entity<RecordAssociation>(entity =>
|
modelBuilder.Entity<KeyAssociation>(entity =>
|
||||||
{
|
{
|
||||||
entity.ToTable("RecordAssociations");
|
entity.ToTable("KeyAssociations");
|
||||||
|
|
||||||
entity.HasKey(e => e.Id);
|
entity.HasKey(e => e.Id);
|
||||||
|
|
||||||
entity.Property(e => e.SourceName)
|
entity.Property(e => e.KeyValue)
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500);
|
||||||
|
|
||||||
|
entity.Property(e => e.SourceKeyField)
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(200);
|
.HasMaxLength(200);
|
||||||
|
|
||||||
entity.Property(e => e.SourceType)
|
entity.Property(e => e.DestinationKeyField)
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(50);
|
.HasMaxLength(200);
|
||||||
|
|
||||||
entity.Property(e => e.SourceKey)
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(500);
|
|
||||||
|
|
||||||
entity.Property(e => e.DestinationEntity)
|
entity.Property(e => e.DestinationEntity)
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
@@ -117,6 +117,9 @@ public class CredentialDbContext : DbContext
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(100);
|
.HasMaxLength(100);
|
||||||
|
|
||||||
|
entity.Property(e => e.SourcesInfo)
|
||||||
|
.HasMaxLength(2000);
|
||||||
|
|
||||||
entity.Property(e => e.AdditionalInfo)
|
entity.Property(e => e.AdditionalInfo)
|
||||||
.HasMaxLength(2000);
|
.HasMaxLength(2000);
|
||||||
|
|
||||||
@@ -125,15 +128,18 @@ public class CredentialDbContext : DbContext
|
|||||||
.HasDefaultValue(true);
|
.HasDefaultValue(true);
|
||||||
|
|
||||||
// Indici
|
// Indici
|
||||||
entity.HasIndex(e => new { e.SourceName, e.SourceKey, e.DestinationEntity })
|
entity.HasIndex(e => e.KeyValue)
|
||||||
.IsUnique()
|
.HasDatabaseName("IX_KeyAssociations_KeyValue");
|
||||||
.HasDatabaseName("IX_RecordAssociations_Unique");
|
|
||||||
|
entity.HasIndex(e => new { e.KeyValue, e.DestinationEntity, e.RestCredentialName })
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("IX_KeyAssociations_Unique");
|
||||||
|
|
||||||
entity.HasIndex(e => e.SourceType);
|
|
||||||
entity.HasIndex(e => e.DestinationEntity);
|
entity.HasIndex(e => e.DestinationEntity);
|
||||||
entity.HasIndex(e => e.RestCredentialName);
|
entity.HasIndex(e => e.RestCredentialName);
|
||||||
entity.HasIndex(e => e.IsActive);
|
entity.HasIndex(e => e.IsActive);
|
||||||
entity.HasIndex(e => e.CreatedAt);
|
entity.HasIndex(e => e.CreatedAt);
|
||||||
|
entity.HasIndex(e => e.LastVerifiedAt);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace CredentialManager.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class AddRestServiceType : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.AddColumn<string>(
|
|
||||||
name: "RestServiceType",
|
|
||||||
table: "Credentials",
|
|
||||||
type: "TEXT",
|
|
||||||
maxLength: 50,
|
|
||||||
nullable: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "RestServiceType",
|
|
||||||
table: "Credentials");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
namespace CredentialManager.Migrations
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Aggiunge la tabella RecordAssociations per tracciare le associazioni tra record sorgente e destinazione
|
|
||||||
/// </summary>
|
|
||||||
public partial class AddRecordAssociations : Migration
|
|
||||||
{
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "RecordAssociations",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
|
||||||
.Annotation("Sqlite:Autoincrement", true),
|
|
||||||
SourceName = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
|
||||||
SourceType = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
|
|
||||||
SourceKey = table.Column<string>(type: "TEXT", maxLength: 500, nullable: false),
|
|
||||||
DestinationEntity = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
|
||||||
DestinationId = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
|
||||||
RestCredentialName = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
|
||||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')"),
|
|
||||||
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
|
|
||||||
IsActive = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: true),
|
|
||||||
AdditionalInfo = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_RecordAssociations", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Indici per migliorare le performance
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_RecordAssociations_Unique",
|
|
||||||
table: "RecordAssociations",
|
|
||||||
columns: new[] { "SourceName", "SourceKey", "DestinationEntity" },
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_RecordAssociations_SourceType",
|
|
||||||
table: "RecordAssociations",
|
|
||||||
column: "SourceType");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_RecordAssociations_DestinationEntity",
|
|
||||||
table: "RecordAssociations",
|
|
||||||
column: "DestinationEntity");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_RecordAssociations_RestCredentialName",
|
|
||||||
table: "RecordAssociations",
|
|
||||||
column: "RestCredentialName");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_RecordAssociations_IsActive",
|
|
||||||
table: "RecordAssociations",
|
|
||||||
column: "IsActive");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_RecordAssociations_CreatedAt",
|
|
||||||
table: "RecordAssociations",
|
|
||||||
column: "CreatedAt");
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "RecordAssociations");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CredentialManager/Migrations/20250629181214_ReplaceRecordAssociationsWithKeyAssociations.Designer.cs
Generated
+211
@@ -0,0 +1,211 @@
|
|||||||
|
// <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("20250629181214_ReplaceRecordAssociationsWithKeyAssociations")]
|
||||||
|
partial class ReplaceRecordAssociationsWithKeyAssociations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "9.0.6");
|
||||||
|
|
||||||
|
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.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);
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+139
@@ -0,0 +1,139 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace CredentialManager.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class ReplaceRecordAssociationsWithKeyAssociations : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Credentials",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
||||||
|
Type = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
|
||||||
|
DatabaseType = table.Column<string>(type: "TEXT", maxLength: 50, nullable: true),
|
||||||
|
ConnectionString = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
|
||||||
|
Host = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||||
|
Port = table.Column<int>(type: "INTEGER", nullable: true),
|
||||||
|
DatabaseName = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||||
|
Username = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||||
|
EncryptedPassword = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
EncryptedApiKey = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
|
||||||
|
EncryptedAuthToken = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
|
||||||
|
CommandTimeout = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 30),
|
||||||
|
TimeoutSeconds = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 100),
|
||||||
|
IgnoreSslErrors = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
|
||||||
|
RestServiceType = table.Column<string>(type: "TEXT", maxLength: 50, nullable: true),
|
||||||
|
Headers = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true),
|
||||||
|
AdditionalParameters = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||||
|
CreatedBy = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||||
|
IsActive = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Credentials", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "KeyAssociations",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
KeyValue = table.Column<string>(type: "TEXT", maxLength: 500, nullable: false),
|
||||||
|
SourceKeyField = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||||
|
DestinationKeyField = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||||
|
DestinationEntity = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||||
|
DestinationId = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||||
|
RestCredentialName = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||||
|
LastVerifiedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||||
|
IsActive = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: true),
|
||||||
|
SourcesInfo = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true),
|
||||||
|
AdditionalInfo = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_KeyAssociations", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Credentials_DatabaseType",
|
||||||
|
table: "Credentials",
|
||||||
|
column: "DatabaseType");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Credentials_IsActive",
|
||||||
|
table: "Credentials",
|
||||||
|
column: "IsActive");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Credentials_Name",
|
||||||
|
table: "Credentials",
|
||||||
|
column: "Name",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Credentials_Type",
|
||||||
|
table: "Credentials",
|
||||||
|
column: "Type");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_KeyAssociations_CreatedAt",
|
||||||
|
table: "KeyAssociations",
|
||||||
|
column: "CreatedAt");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_KeyAssociations_DestinationEntity",
|
||||||
|
table: "KeyAssociations",
|
||||||
|
column: "DestinationEntity");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_KeyAssociations_IsActive",
|
||||||
|
table: "KeyAssociations",
|
||||||
|
column: "IsActive");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_KeyAssociations_KeyValue",
|
||||||
|
table: "KeyAssociations",
|
||||||
|
column: "KeyValue");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_KeyAssociations_LastVerifiedAt",
|
||||||
|
table: "KeyAssociations",
|
||||||
|
column: "LastVerifiedAt");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_KeyAssociations_RestCredentialName",
|
||||||
|
table: "KeyAssociations",
|
||||||
|
column: "RestCredentialName");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_KeyAssociations_Unique",
|
||||||
|
table: "KeyAssociations",
|
||||||
|
columns: new[] { "KeyValue", "DestinationEntity", "RestCredentialName" },
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Credentials");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "KeyAssociations");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using CredentialManager.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace CredentialManager.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(CredentialDbContext))]
|
||||||
|
partial class CredentialDbContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "9.0.6");
|
||||||
|
|
||||||
|
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.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);
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
-19
@@ -3,33 +3,38 @@ using System.ComponentModel.DataAnnotations;
|
|||||||
namespace CredentialManager.Models;
|
namespace CredentialManager.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Entità per memorizzare le associazioni tra record sorgente e destinazione
|
/// Entità per memorizzare le associazioni basate sui valori delle chiavi
|
||||||
|
/// Un'associazione lega un valore di chiave a un record di destinazione,
|
||||||
|
/// indipendentemente dalla sorgente che ha generato quel valore
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class RecordAssociation
|
public class KeyAssociation
|
||||||
{
|
{
|
||||||
[Key]
|
[Key]
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Nome della sorgente dati (nome tabella/file/foglio)
|
/// Valore della chiave che identifica univocamente l'oggetto business
|
||||||
/// </summary>
|
/// (es: "CUST001", "12345", "ABC-DEF-GHI")
|
||||||
[Required]
|
|
||||||
[MaxLength(200)]
|
|
||||||
public string SourceName { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tipo di sorgente (database, file)
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
[MaxLength(50)]
|
|
||||||
public string SourceType { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Chiave del record sorgente (può essere un ID o una combinazione di campi)
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Required]
|
[Required]
|
||||||
[MaxLength(500)]
|
[MaxLength(500)]
|
||||||
public string SourceKey { get; set; } = string.Empty;
|
public string KeyValue { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Nome del campo chiave nella sorgente
|
||||||
|
/// (es: "CustomerCode", "ID", "ArticleNumber")
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[MaxLength(200)]
|
||||||
|
public string SourceKeyField { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Nome del campo chiave nella destinazione
|
||||||
|
/// (es: "CardCode", "DocEntry", "ItemCode")
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[MaxLength(200)]
|
||||||
|
public string DestinationKeyField { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Nome dell'entità di destinazione
|
/// Nome dell'entità di destinazione
|
||||||
@@ -46,7 +51,7 @@ public class RecordAssociation
|
|||||||
public string DestinationId { get; set; } = string.Empty;
|
public string DestinationId { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Nome della credenziale REST utilizzata
|
/// Nome della credenziale REST utilizzata per la destinazione
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Required]
|
[Required]
|
||||||
[MaxLength(100)]
|
[MaxLength(100)]
|
||||||
@@ -62,11 +67,22 @@ public class RecordAssociation
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime? UpdatedAt { get; set; }
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Data e ora dell'ultima verifica che il record di destinazione esiste ancora
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? LastVerifiedAt { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indica se l'associazione è ancora attiva
|
/// Indica se l'associazione è ancora attiva
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsActive { get; set; } = true;
|
public bool IsActive { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Informazioni aggiuntive sui record che hanno contribuito a questa associazione
|
||||||
|
/// </summary>
|
||||||
|
[MaxLength(2000)]
|
||||||
|
public string? SourcesInfo { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Informazioni aggiuntive in formato JSON
|
/// Informazioni aggiuntive in formato JSON
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -68,16 +68,16 @@ public class DatabaseInitializer : IDatabaseInitializer
|
|||||||
await _context.Credentials.CountAsync();
|
await _context.Credentials.CountAsync();
|
||||||
_logger.LogInformation("Tabella Credentials verificata con successo");
|
_logger.LogInformation("Tabella Credentials verificata con successo");
|
||||||
|
|
||||||
// Verifica se la tabella RecordAssociations esiste, se non esiste la crea senza ricreare tutto il database
|
// Verifica se la tabella KeyAssociations esiste, se non esiste la crea senza ricreare tutto il database
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _context.RecordAssociations.CountAsync();
|
await _context.KeyAssociations.CountAsync();
|
||||||
_logger.LogInformation("Tabella RecordAssociations verificata con successo");
|
_logger.LogInformation("Tabella KeyAssociations verificata con successo");
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Tabella RecordAssociations non trovata, creazione tramite migrazione...");
|
_logger.LogInformation("Tabella KeyAssociations non trovata, creazione tramite migrazione...");
|
||||||
await CreateRecordAssociationsTableAsync();
|
await CreateKeyAssociationsTableAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
@@ -170,60 +170,78 @@ public class DatabaseInitializer : IDatabaseInitializer
|
|||||||
_logger.LogInformation("Colonna RestServiceType aggiunta con successo");
|
_logger.LogInformation("Colonna RestServiceType aggiunta con successo");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrazione 2: Verifica se la tabella RecordAssociations esiste
|
// Migrazione 2: Elimina vecchia tabella RecordAssociations se esiste e crea KeyAssociations
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Prova a eliminare la vecchia tabella se esiste
|
||||||
|
await _context.Database.ExecuteSqlRawAsync("DROP TABLE IF EXISTS RecordAssociations");
|
||||||
|
_logger.LogInformation("Vecchia tabella RecordAssociations eliminata");
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Ignora errori se la tabella non esiste
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica se la tabella KeyAssociations esiste
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _context.Database.ExecuteSqlRawAsync(
|
await _context.Database.ExecuteSqlRawAsync(
|
||||||
"SELECT COUNT(*) FROM RecordAssociations LIMIT 1");
|
"SELECT COUNT(*) FROM KeyAssociations LIMIT 1");
|
||||||
_logger.LogInformation("Tabella RecordAssociations già presente");
|
_logger.LogInformation("Tabella KeyAssociations già presente");
|
||||||
}
|
}
|
||||||
catch (Microsoft.Data.Sqlite.SqliteException)
|
catch (Microsoft.Data.Sqlite.SqliteException)
|
||||||
{
|
{
|
||||||
// La tabella non esiste, la creiamo
|
// La tabella non esiste, la creiamo
|
||||||
_logger.LogInformation("Creazione tabella RecordAssociations...");
|
_logger.LogInformation("Creazione tabella KeyAssociations...");
|
||||||
|
|
||||||
// Crea la tabella
|
// Crea la tabella
|
||||||
await _context.Database.ExecuteSqlRawAsync(@"
|
await _context.Database.ExecuteSqlRawAsync(@"
|
||||||
CREATE TABLE RecordAssociations (
|
CREATE TABLE KeyAssociations (
|
||||||
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
SourceName TEXT NOT NULL,
|
KeyValue TEXT NOT NULL,
|
||||||
SourceType TEXT NOT NULL,
|
SourceKeyField TEXT NOT NULL,
|
||||||
SourceKey TEXT NOT NULL,
|
DestinationKeyField TEXT NOT NULL,
|
||||||
DestinationEntity TEXT NOT NULL,
|
DestinationEntity TEXT NOT NULL,
|
||||||
DestinationId TEXT NOT NULL,
|
DestinationId TEXT NOT NULL,
|
||||||
RestCredentialName TEXT NOT NULL,
|
RestCredentialName TEXT NOT NULL,
|
||||||
CreatedAt TEXT NOT NULL DEFAULT (datetime('now')),
|
CreatedAt TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
UpdatedAt TEXT,
|
UpdatedAt TEXT,
|
||||||
|
LastVerifiedAt TEXT,
|
||||||
IsActive INTEGER NOT NULL DEFAULT 1,
|
IsActive INTEGER NOT NULL DEFAULT 1,
|
||||||
|
SourcesInfo TEXT,
|
||||||
AdditionalInfo TEXT
|
AdditionalInfo TEXT
|
||||||
)");
|
)");
|
||||||
|
|
||||||
// Crea gli indici
|
// Crea gli indici
|
||||||
await _context.Database.ExecuteSqlRawAsync(@"
|
await _context.Database.ExecuteSqlRawAsync(@"
|
||||||
CREATE UNIQUE INDEX IX_RecordAssociations_Unique
|
CREATE INDEX IX_KeyAssociations_KeyValue
|
||||||
ON RecordAssociations (SourceName, SourceKey, DestinationEntity)");
|
ON KeyAssociations (KeyValue)");
|
||||||
|
|
||||||
await _context.Database.ExecuteSqlRawAsync(@"
|
await _context.Database.ExecuteSqlRawAsync(@"
|
||||||
CREATE INDEX IX_RecordAssociations_SourceType
|
CREATE UNIQUE INDEX IX_KeyAssociations_Unique
|
||||||
ON RecordAssociations (SourceType)");
|
ON KeyAssociations (KeyValue, DestinationEntity, RestCredentialName)");
|
||||||
|
|
||||||
await _context.Database.ExecuteSqlRawAsync(@"
|
await _context.Database.ExecuteSqlRawAsync(@"
|
||||||
CREATE INDEX IX_RecordAssociations_DestinationEntity
|
CREATE INDEX IX_KeyAssociations_DestinationEntity
|
||||||
ON RecordAssociations (DestinationEntity)");
|
ON KeyAssociations (DestinationEntity)");
|
||||||
|
|
||||||
await _context.Database.ExecuteSqlRawAsync(@"
|
await _context.Database.ExecuteSqlRawAsync(@"
|
||||||
CREATE INDEX IX_RecordAssociations_RestCredentialName
|
CREATE INDEX IX_KeyAssociations_RestCredentialName
|
||||||
ON RecordAssociations (RestCredentialName)");
|
ON KeyAssociations (RestCredentialName)");
|
||||||
|
|
||||||
await _context.Database.ExecuteSqlRawAsync(@"
|
await _context.Database.ExecuteSqlRawAsync(@"
|
||||||
CREATE INDEX IX_RecordAssociations_IsActive
|
CREATE INDEX IX_KeyAssociations_IsActive
|
||||||
ON RecordAssociations (IsActive)");
|
ON KeyAssociations (IsActive)");
|
||||||
|
|
||||||
await _context.Database.ExecuteSqlRawAsync(@"
|
await _context.Database.ExecuteSqlRawAsync(@"
|
||||||
CREATE INDEX IX_RecordAssociations_CreatedAt
|
CREATE INDEX IX_KeyAssociations_CreatedAt
|
||||||
ON RecordAssociations (CreatedAt)");
|
ON KeyAssociations (CreatedAt)");
|
||||||
|
|
||||||
_logger.LogInformation("Tabella RecordAssociations creata con successo");
|
await _context.Database.ExecuteSqlRawAsync(@"
|
||||||
|
CREATE INDEX IX_KeyAssociations_LastVerifiedAt
|
||||||
|
ON KeyAssociations (LastVerifiedAt)");
|
||||||
|
|
||||||
|
_logger.LogInformation("Tabella KeyAssociations creata con successo");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -233,58 +251,67 @@ public class DatabaseInitializer : IDatabaseInitializer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CreateRecordAssociationsTableAsync()
|
private async Task CreateKeyAssociationsTableAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Creazione tabella RecordAssociations...");
|
_logger.LogInformation("Creazione tabella KeyAssociations...");
|
||||||
|
|
||||||
// Crea la tabella
|
// Elimina la vecchia tabella se esiste
|
||||||
|
await _context.Database.ExecuteSqlRawAsync("DROP TABLE IF EXISTS RecordAssociations");
|
||||||
|
|
||||||
|
// Crea la nuova tabella
|
||||||
await _context.Database.ExecuteSqlRawAsync(@"
|
await _context.Database.ExecuteSqlRawAsync(@"
|
||||||
CREATE TABLE RecordAssociations (
|
CREATE TABLE KeyAssociations (
|
||||||
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
SourceName TEXT NOT NULL,
|
KeyValue TEXT NOT NULL,
|
||||||
SourceType TEXT NOT NULL,
|
SourceKeyField TEXT NOT NULL,
|
||||||
SourceKey TEXT NOT NULL,
|
DestinationKeyField TEXT NOT NULL,
|
||||||
DestinationEntity TEXT NOT NULL,
|
DestinationEntity TEXT NOT NULL,
|
||||||
DestinationId TEXT NOT NULL,
|
DestinationId TEXT NOT NULL,
|
||||||
RestCredentialName TEXT NOT NULL,
|
RestCredentialName TEXT NOT NULL,
|
||||||
CreatedAt TEXT NOT NULL DEFAULT (datetime('now')),
|
CreatedAt TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
UpdatedAt TEXT,
|
UpdatedAt TEXT,
|
||||||
|
LastVerifiedAt TEXT,
|
||||||
IsActive INTEGER NOT NULL DEFAULT 1,
|
IsActive INTEGER NOT NULL DEFAULT 1,
|
||||||
|
SourcesInfo TEXT,
|
||||||
AdditionalInfo TEXT
|
AdditionalInfo TEXT
|
||||||
)");
|
)");
|
||||||
|
|
||||||
// Crea gli indici
|
// Crea gli indici
|
||||||
await _context.Database.ExecuteSqlRawAsync(@"
|
await _context.Database.ExecuteSqlRawAsync(@"
|
||||||
CREATE UNIQUE INDEX IX_RecordAssociations_Unique
|
CREATE INDEX IX_KeyAssociations_KeyValue
|
||||||
ON RecordAssociations (SourceName, SourceKey, DestinationEntity)");
|
ON KeyAssociations (KeyValue)");
|
||||||
|
|
||||||
await _context.Database.ExecuteSqlRawAsync(@"
|
await _context.Database.ExecuteSqlRawAsync(@"
|
||||||
CREATE INDEX IX_RecordAssociations_SourceType
|
CREATE UNIQUE INDEX IX_KeyAssociations_Unique
|
||||||
ON RecordAssociations (SourceType)");
|
ON KeyAssociations (KeyValue, DestinationEntity, RestCredentialName)");
|
||||||
|
|
||||||
await _context.Database.ExecuteSqlRawAsync(@"
|
await _context.Database.ExecuteSqlRawAsync(@"
|
||||||
CREATE INDEX IX_RecordAssociations_DestinationEntity
|
CREATE INDEX IX_KeyAssociations_DestinationEntity
|
||||||
ON RecordAssociations (DestinationEntity)");
|
ON KeyAssociations (DestinationEntity)");
|
||||||
|
|
||||||
await _context.Database.ExecuteSqlRawAsync(@"
|
await _context.Database.ExecuteSqlRawAsync(@"
|
||||||
CREATE INDEX IX_RecordAssociations_RestCredentialName
|
CREATE INDEX IX_KeyAssociations_RestCredentialName
|
||||||
ON RecordAssociations (RestCredentialName)");
|
ON KeyAssociations (RestCredentialName)");
|
||||||
|
|
||||||
await _context.Database.ExecuteSqlRawAsync(@"
|
await _context.Database.ExecuteSqlRawAsync(@"
|
||||||
CREATE INDEX IX_RecordAssociations_IsActive
|
CREATE INDEX IX_KeyAssociations_IsActive
|
||||||
ON RecordAssociations (IsActive)");
|
ON KeyAssociations (IsActive)");
|
||||||
|
|
||||||
await _context.Database.ExecuteSqlRawAsync(@"
|
await _context.Database.ExecuteSqlRawAsync(@"
|
||||||
CREATE INDEX IX_RecordAssociations_CreatedAt
|
CREATE INDEX IX_KeyAssociations_CreatedAt
|
||||||
ON RecordAssociations (CreatedAt)");
|
ON KeyAssociations (CreatedAt)");
|
||||||
|
|
||||||
_logger.LogInformation("Tabella RecordAssociations creata con successo");
|
await _context.Database.ExecuteSqlRawAsync(@"
|
||||||
|
CREATE INDEX IX_KeyAssociations_LastVerifiedAt
|
||||||
|
ON KeyAssociations (LastVerifiedAt)");
|
||||||
|
|
||||||
|
_logger.LogInformation("Tabella KeyAssociations creata con successo");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Errore nella creazione della tabella RecordAssociations");
|
_logger.LogError(ex, "Errore nella creazione della tabella KeyAssociations");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
using CredentialManager.Models;
|
||||||
|
|
||||||
|
namespace CredentialManager.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interfaccia per il servizio di gestione delle associazioni basate sui valori delle chiavi
|
||||||
|
/// </summary>
|
||||||
|
public interface IKeyAssociationService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Salva una nuova associazione o aggiorna una esistente
|
||||||
|
/// </summary>
|
||||||
|
Task<int> SaveAssociationAsync(KeyAssociation association);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cerca un'associazione esistente tramite valore chiave
|
||||||
|
/// </summary>
|
||||||
|
Task<KeyAssociation?> FindAssociationByKeyValueAsync(string keyValue, string destinationEntity, string restCredentialName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cerca un'associazione esistente tramite valore chiave, indipendentemente dalla destinazione
|
||||||
|
/// </summary>
|
||||||
|
Task<KeyAssociation?> FindAssociationByKeyValueAsync(string keyValue);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ottiene tutte le associazioni per un'entità di destinazione specifica
|
||||||
|
/// </summary>
|
||||||
|
Task<List<KeyAssociation>> GetAssociationsByDestinationAsync(string destinationEntity, string restCredentialName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ottiene tutte le associazioni attive
|
||||||
|
/// </summary>
|
||||||
|
Task<List<KeyAssociation>> GetAllActiveAssociationsAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ottiene tutte le associazioni (attive e non)
|
||||||
|
/// </summary>
|
||||||
|
Task<List<KeyAssociation>> GetAllAssociationsAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Aggiorna un'associazione esistente
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> UpdateAssociationAsync(KeyAssociation association);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disattiva un'associazione
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> DeactivateAssociationAsync(int id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Elimina definitivamente un'associazione
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> DeleteAssociationAsync(int id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pulisce le associazioni più vecchie di un determinato periodo
|
||||||
|
/// </summary>
|
||||||
|
Task<int> CleanupOldAssociationsAsync(TimeSpan olderThan);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Elimina tutte le associazioni per una specifica combinazione entità-credenziale
|
||||||
|
/// </summary>
|
||||||
|
Task<int> ClearAssociationsAsync(string destinationEntity, string restCredentialName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Elimina tutte le associazioni nel sistema
|
||||||
|
/// </summary>
|
||||||
|
Task<int> ClearAllAssociationsAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifica se un ID di destinazione esiste ancora nel sistema target
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> ValidateDestinationIdAsync(string destinationId, string destinationEntity, string restCredentialName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ottiene tutte le associazioni con ID di destinazione non validi
|
||||||
|
/// </summary>
|
||||||
|
Task<List<KeyAssociation>> GetInvalidAssociationsAsync(string destinationEntity, string restCredentialName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pulisce le associazioni con ID di destinazione non più validi
|
||||||
|
/// </summary>
|
||||||
|
Task<int> CleanupInvalidAssociationsAsync(string destinationEntity, string restCredentialName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Aggiorna la data di ultima verifica per un'associazione
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> UpdateLastVerifiedAsync(int id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ottiene statistiche sulle associazioni
|
||||||
|
/// </summary>
|
||||||
|
Task<AssociationStatistics> GetStatisticsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Statistiche sulle associazioni
|
||||||
|
/// </summary>
|
||||||
|
public class AssociationStatistics
|
||||||
|
{
|
||||||
|
public int TotalAssociations { get; set; }
|
||||||
|
public int ActiveAssociations { get; set; }
|
||||||
|
public int InactiveAssociations { get; set; }
|
||||||
|
public int UniqueKeyValues { get; set; }
|
||||||
|
public int UniqueDestinationEntities { get; set; }
|
||||||
|
public DateTime? OldestAssociation { get; set; }
|
||||||
|
public DateTime? NewestAssociation { get; set; }
|
||||||
|
public Dictionary<string, int> AssociationsByEntity { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
using CredentialManager.Models;
|
|
||||||
|
|
||||||
namespace CredentialManager.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Interfaccia per il servizio di gestione delle associazioni record
|
|
||||||
/// </summary>
|
|
||||||
public interface IRecordAssociationService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Salva una nuova associazione tra record sorgente e destinazione
|
|
||||||
/// </summary>
|
|
||||||
Task<int> SaveAssociationAsync(RecordAssociation association);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Cerca un'associazione esistente tramite chiave sorgente
|
|
||||||
/// </summary>
|
|
||||||
Task<RecordAssociation?> FindAssociationAsync(string sourceName, string sourceKey, string destinationEntity);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ottiene tutte le associazioni per una sorgente specifica
|
|
||||||
/// </summary>
|
|
||||||
Task<List<RecordAssociation>> GetAssociationsBySourceAsync(string sourceName, string sourceType);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ottiene tutte le associazioni per un'entità di destinazione specifica
|
|
||||||
/// </summary>
|
|
||||||
Task<List<RecordAssociation>> GetAssociationsByDestinationAsync(string destinationEntity, string restCredentialName);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ottiene tutte le associazioni attive
|
|
||||||
/// </summary>
|
|
||||||
Task<List<RecordAssociation>> GetAllActiveAssociationsAsync();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Aggiorna un'associazione esistente
|
|
||||||
/// </summary>
|
|
||||||
Task<bool> UpdateAssociationAsync(RecordAssociation association);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Disattiva un'associazione (soft delete)
|
|
||||||
/// </summary>
|
|
||||||
Task<bool> DeactivateAssociationAsync(int id);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Elimina definitivamente un'associazione
|
|
||||||
/// </summary>
|
|
||||||
Task<bool> DeleteAssociationAsync(int id);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Pulisce le associazioni obsolete (opzionale)
|
|
||||||
/// </summary>
|
|
||||||
Task<int> CleanupOldAssociationsAsync(TimeSpan olderThan);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Elimina tutte le associazioni per una specifica combinazione sorgente-destinazione
|
|
||||||
/// </summary>
|
|
||||||
Task<int> ClearAssociationsAsync(string sourceName, string destinationEntity, string restCredentialName);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Elimina tutte le associazioni nel sistema
|
|
||||||
/// </summary>
|
|
||||||
Task<int> ClearAllAssociationsAsync();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifica se un ID di destinazione esiste ancora nel sistema target
|
|
||||||
/// </summary>
|
|
||||||
Task<bool> ValidateDestinationIdAsync(string destinationId, string destinationEntity, string restCredentialName);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ottiene tutte le associazioni con ID di destinazione non validi
|
|
||||||
/// </summary>
|
|
||||||
Task<List<RecordAssociation>> GetInvalidAssociationsAsync(string destinationEntity, string restCredentialName);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Pulisce le associazioni con ID di destinazione non più validi
|
|
||||||
/// </summary>
|
|
||||||
Task<int> CleanupInvalidAssociationsAsync(string destinationEntity, string restCredentialName);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,494 @@
|
|||||||
|
using CredentialManager.Data;
|
||||||
|
using CredentialManager.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace CredentialManager.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Servizio per la gestione delle associazioni basate sui valori delle chiavi
|
||||||
|
/// </summary>
|
||||||
|
public class KeyAssociationService : IKeyAssociationService
|
||||||
|
{
|
||||||
|
private readonly CredentialDbContext _context;
|
||||||
|
private readonly ILogger<KeyAssociationService> _logger;
|
||||||
|
|
||||||
|
public KeyAssociationService(
|
||||||
|
CredentialDbContext context,
|
||||||
|
ILogger<KeyAssociationService> logger)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> SaveAssociationAsync(KeyAssociation association)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("DEBUG: Tentativo salvataggio associazione - KeyValue: '{KeyValue}', DestinationEntity: '{DestinationEntity}', DestinationId: '{DestinationId}', RestCredentialName: '{RestCredentialName}'",
|
||||||
|
association.KeyValue, association.DestinationEntity, association.DestinationId, association.RestCredentialName);
|
||||||
|
|
||||||
|
// Controlla se esiste già un'associazione per questo valore chiave e destinazione
|
||||||
|
var existing = await _context.KeyAssociations
|
||||||
|
.FirstOrDefaultAsync(ka =>
|
||||||
|
ka.KeyValue == association.KeyValue &&
|
||||||
|
ka.DestinationEntity == association.DestinationEntity &&
|
||||||
|
ka.RestCredentialName == association.RestCredentialName &&
|
||||||
|
ka.IsActive);
|
||||||
|
|
||||||
|
_logger.LogInformation("DEBUG: Controllo associazione esistente: {Found}. ID: {Id}",
|
||||||
|
existing != null, existing?.Id);
|
||||||
|
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
// Aggiorna l'associazione esistente
|
||||||
|
existing.DestinationId = association.DestinationId;
|
||||||
|
existing.SourceKeyField = association.SourceKeyField;
|
||||||
|
existing.DestinationKeyField = association.DestinationKeyField;
|
||||||
|
existing.UpdatedAt = DateTime.UtcNow;
|
||||||
|
existing.LastVerifiedAt = DateTime.UtcNow;
|
||||||
|
existing.AdditionalInfo = association.AdditionalInfo;
|
||||||
|
|
||||||
|
// Aggiorna le informazioni sulle sorgenti
|
||||||
|
UpdateSourcesInfo(existing, association);
|
||||||
|
|
||||||
|
_context.KeyAssociations.Update(existing);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
_logger.LogInformation("Associazione aggiornata: KeyValue={KeyValue} -> {DestinationEntity}/{DestinationId}",
|
||||||
|
association.KeyValue, association.DestinationEntity, association.DestinationId);
|
||||||
|
|
||||||
|
return existing.Id;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Crea nuova associazione
|
||||||
|
association.CreatedAt = DateTime.UtcNow;
|
||||||
|
association.LastVerifiedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
_context.KeyAssociations.Add(association);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
_logger.LogInformation("Nuova associazione creata: KeyValue={KeyValue} -> {DestinationEntity}/{DestinationId}",
|
||||||
|
association.KeyValue, association.DestinationEntity, association.DestinationId);
|
||||||
|
|
||||||
|
return association.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Errore nel salvare l'associazione: KeyValue={KeyValue} -> {DestinationEntity}",
|
||||||
|
association.KeyValue, association.DestinationEntity);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<KeyAssociation?> FindAssociationByKeyValueAsync(string keyValue, string destinationEntity, string restCredentialName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("DEBUG: Ricerca associazione con parametri - KeyValue: '{KeyValue}', DestinationEntity: '{DestinationEntity}', RestCredentialName: '{RestCredentialName}'",
|
||||||
|
keyValue, destinationEntity, restCredentialName);
|
||||||
|
|
||||||
|
var result = await _context.KeyAssociations
|
||||||
|
.FirstOrDefaultAsync(ka =>
|
||||||
|
ka.KeyValue == keyValue &&
|
||||||
|
ka.DestinationEntity == destinationEntity &&
|
||||||
|
ka.RestCredentialName == restCredentialName &&
|
||||||
|
ka.IsActive);
|
||||||
|
|
||||||
|
_logger.LogInformation("DEBUG: Risultato ricerca associazione: {Found}. ID: {Id}, DestinationId: '{DestinationId}'",
|
||||||
|
result != null, result?.Id, result?.DestinationId);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Errore nella ricerca dell'associazione: KeyValue={KeyValue} -> {DestinationEntity}",
|
||||||
|
keyValue, destinationEntity);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<KeyAssociation?> FindAssociationByKeyValueAsync(string keyValue)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _context.KeyAssociations
|
||||||
|
.Where(ka => ka.KeyValue == keyValue && ka.IsActive)
|
||||||
|
.OrderByDescending(ka => ka.UpdatedAt ?? ka.CreatedAt)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Errore nella ricerca dell'associazione per KeyValue={KeyValue}", keyValue);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<KeyAssociation>> GetAssociationsByDestinationAsync(string destinationEntity, string restCredentialName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _context.KeyAssociations
|
||||||
|
.Where(ka => ka.DestinationEntity == destinationEntity &&
|
||||||
|
ka.RestCredentialName == restCredentialName &&
|
||||||
|
ka.IsActive)
|
||||||
|
.OrderByDescending(ka => ka.CreatedAt)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Errore nel recupero delle associazioni per destinazione: {DestinationEntity} ({RestCredentialName})",
|
||||||
|
destinationEntity, restCredentialName);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<KeyAssociation>> GetAllActiveAssociationsAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _context.KeyAssociations
|
||||||
|
.Where(ka => ka.IsActive)
|
||||||
|
.OrderByDescending(ka => ka.CreatedAt)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Errore nel recupero di tutte le associazioni attive");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<KeyAssociation>> GetAllAssociationsAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _context.KeyAssociations
|
||||||
|
.OrderByDescending(ka => ka.CreatedAt)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Errore nel recupero di tutte le associazioni");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UpdateAssociationAsync(KeyAssociation association)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var existing = await _context.KeyAssociations.FindAsync(association.Id);
|
||||||
|
if (existing == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Associazione con ID {Id} non trovata per l'aggiornamento", association.Id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
existing.KeyValue = association.KeyValue;
|
||||||
|
existing.SourceKeyField = association.SourceKeyField;
|
||||||
|
existing.DestinationKeyField = association.DestinationKeyField;
|
||||||
|
existing.DestinationId = association.DestinationId;
|
||||||
|
existing.RestCredentialName = association.RestCredentialName;
|
||||||
|
existing.UpdatedAt = DateTime.UtcNow;
|
||||||
|
existing.AdditionalInfo = association.AdditionalInfo;
|
||||||
|
existing.SourcesInfo = association.SourcesInfo;
|
||||||
|
existing.IsActive = association.IsActive;
|
||||||
|
|
||||||
|
_context.KeyAssociations.Update(existing);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
_logger.LogInformation("Associazione aggiornata: ID {Id}", association.Id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Errore nell'aggiornamento dell'associazione: ID {Id}", association.Id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeactivateAssociationAsync(int id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var association = await _context.KeyAssociations.FindAsync(id);
|
||||||
|
if (association == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Associazione con ID {Id} non trovata per la disattivazione", id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
association.IsActive = false;
|
||||||
|
association.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
_context.KeyAssociations.Update(association);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
_logger.LogInformation("Associazione disattivata: ID {Id}", id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Errore nella disattivazione dell'associazione: ID {Id}", id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteAssociationAsync(int id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var association = await _context.KeyAssociations.FindAsync(id);
|
||||||
|
if (association == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Associazione con ID {Id} non trovata per l'eliminazione", id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_context.KeyAssociations.Remove(association);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
_logger.LogInformation("Associazione eliminata: ID {Id}", id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Errore nell'eliminazione dell'associazione: ID {Id}", id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> CleanupOldAssociationsAsync(TimeSpan olderThan)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cutoffDate = DateTime.UtcNow - olderThan;
|
||||||
|
var oldAssociations = await _context.KeyAssociations
|
||||||
|
.Where(ka => ka.CreatedAt < cutoffDate && !ka.IsActive)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (oldAssociations.Any())
|
||||||
|
{
|
||||||
|
_context.KeyAssociations.RemoveRange(oldAssociations);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
_logger.LogInformation("Pulite {Count} associazioni obsolete più vecchie di {Cutoff}",
|
||||||
|
oldAssociations.Count, cutoffDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return oldAssociations.Count;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Errore nella pulizia delle associazioni obsolete");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> ClearAssociationsAsync(string destinationEntity, string restCredentialName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var associationsToDelete = await _context.KeyAssociations
|
||||||
|
.Where(ka => ka.DestinationEntity == destinationEntity &&
|
||||||
|
ka.RestCredentialName == restCredentialName)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (associationsToDelete.Any())
|
||||||
|
{
|
||||||
|
_context.KeyAssociations.RemoveRange(associationsToDelete);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
_logger.LogInformation("Eliminate {Count} associazioni per {DestinationEntity}",
|
||||||
|
associationsToDelete.Count, destinationEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
return associationsToDelete.Count;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Errore nella cancellazione delle associazioni per {DestinationEntity}",
|
||||||
|
destinationEntity);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> ClearAllAssociationsAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var allAssociations = await _context.KeyAssociations.ToListAsync();
|
||||||
|
var count = allAssociations.Count;
|
||||||
|
|
||||||
|
if (allAssociations.Any())
|
||||||
|
{
|
||||||
|
_context.KeyAssociations.RemoveRange(allAssociations);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
_logger.LogWarning("Eliminate TUTTE le {Count} associazioni dal sistema", count);
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Errore nella cancellazione di tutte le associazioni");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ValidateDestinationIdAsync(string destinationId, string destinationEntity, string restCredentialName)
|
||||||
|
{
|
||||||
|
// Questa implementazione base restituisce sempre true
|
||||||
|
// Dovrebbe essere estesa per verificare effettivamente l'esistenza nel sistema REST
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// TODO: Implementare la logica di validazione effettiva con il servizio REST
|
||||||
|
// Per ora assumiamo che l'ID sia valido
|
||||||
|
_logger.LogDebug("Validazione ID destinazione {DestinationId} per entità {DestinationEntity} - Non implementata",
|
||||||
|
destinationId, destinationEntity);
|
||||||
|
|
||||||
|
return await Task.FromResult(true);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Errore nella validazione dell'ID destinazione {DestinationId}", destinationId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<KeyAssociation>> GetInvalidAssociationsAsync(string destinationEntity, string restCredentialName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var associations = await _context.KeyAssociations
|
||||||
|
.Where(ka => ka.DestinationEntity == destinationEntity &&
|
||||||
|
ka.RestCredentialName == restCredentialName &&
|
||||||
|
ka.IsActive)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var invalidAssociations = new List<KeyAssociation>();
|
||||||
|
|
||||||
|
// Verifica ogni associazione
|
||||||
|
foreach (var association in associations)
|
||||||
|
{
|
||||||
|
var isValid = await ValidateDestinationIdAsync(association.DestinationId, destinationEntity, restCredentialName);
|
||||||
|
if (!isValid)
|
||||||
|
{
|
||||||
|
invalidAssociations.Add(association);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Trovate {Invalid}/{Total} associazioni non valide per {DestinationEntity}",
|
||||||
|
invalidAssociations.Count, associations.Count, destinationEntity);
|
||||||
|
|
||||||
|
return invalidAssociations;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Errore nel recupero delle associazioni non valide per {DestinationEntity}", destinationEntity);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> CleanupInvalidAssociationsAsync(string destinationEntity, string restCredentialName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var invalidAssociations = await GetInvalidAssociationsAsync(destinationEntity, restCredentialName);
|
||||||
|
|
||||||
|
if (invalidAssociations.Any())
|
||||||
|
{
|
||||||
|
_context.KeyAssociations.RemoveRange(invalidAssociations);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
_logger.LogWarning("Eliminate {Count} associazioni non valide per {DestinationEntity}",
|
||||||
|
invalidAssociations.Count, destinationEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
return invalidAssociations.Count;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Errore nella pulizia delle associazioni non valide per {DestinationEntity}", destinationEntity);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UpdateLastVerifiedAsync(int id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var association = await _context.KeyAssociations.FindAsync(id);
|
||||||
|
if (association == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Associazione con ID {Id} non trovata per l'aggiornamento della verifica", id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
association.LastVerifiedAt = DateTime.UtcNow;
|
||||||
|
_context.KeyAssociations.Update(association);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Errore nell'aggiornamento della verifica per associazione: ID {Id}", id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AssociationStatistics> GetStatisticsAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var allAssociations = await _context.KeyAssociations.ToListAsync();
|
||||||
|
|
||||||
|
var stats = new AssociationStatistics
|
||||||
|
{
|
||||||
|
TotalAssociations = allAssociations.Count,
|
||||||
|
ActiveAssociations = allAssociations.Count(a => a.IsActive),
|
||||||
|
InactiveAssociations = allAssociations.Count(a => !a.IsActive),
|
||||||
|
UniqueKeyValues = allAssociations.Select(a => a.KeyValue).Distinct().Count(),
|
||||||
|
UniqueDestinationEntities = allAssociations.Select(a => a.DestinationEntity).Distinct().Count(),
|
||||||
|
OldestAssociation = allAssociations.Any() ? allAssociations.Min(a => a.CreatedAt) : null,
|
||||||
|
NewestAssociation = allAssociations.Any() ? allAssociations.Max(a => a.CreatedAt) : null,
|
||||||
|
AssociationsByEntity = allAssociations
|
||||||
|
.GroupBy(a => a.DestinationEntity)
|
||||||
|
.ToDictionary(g => g.Key, g => g.Count())
|
||||||
|
};
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Errore nel calcolo delle statistiche delle associazioni");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateSourcesInfo(KeyAssociation existing, KeyAssociation newAssociation)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var sourcesInfo = existing.SourcesInfo ?? "";
|
||||||
|
var timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm");
|
||||||
|
var newSourceInfo = $"{timestamp}: {newAssociation.SourceKeyField}";
|
||||||
|
|
||||||
|
if (!sourcesInfo.Contains(newSourceInfo))
|
||||||
|
{
|
||||||
|
existing.SourcesInfo = string.IsNullOrEmpty(sourcesInfo)
|
||||||
|
? newSourceInfo
|
||||||
|
: $"{sourcesInfo}; {newSourceInfo}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Errore nell'aggiornamento delle informazioni sulle sorgenti");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,381 +0,0 @@
|
|||||||
using CredentialManager.Data;
|
|
||||||
using CredentialManager.Models;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace CredentialManager.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Servizio per la gestione delle associazioni tra record sorgente e destinazione
|
|
||||||
/// </summary>
|
|
||||||
public class RecordAssociationService : IRecordAssociationService
|
|
||||||
{
|
|
||||||
private readonly CredentialDbContext _context;
|
|
||||||
private readonly ILogger<RecordAssociationService> _logger;
|
|
||||||
|
|
||||||
public RecordAssociationService(
|
|
||||||
CredentialDbContext context,
|
|
||||||
ILogger<RecordAssociationService> logger)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<int> SaveAssociationAsync(RecordAssociation association)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Controlla se esiste già un'associazione per questa combinazione
|
|
||||||
var existing = await _context.RecordAssociations
|
|
||||||
.FirstOrDefaultAsync(ra =>
|
|
||||||
ra.SourceName == association.SourceName &&
|
|
||||||
ra.SourceKey == association.SourceKey &&
|
|
||||||
ra.DestinationEntity == association.DestinationEntity &&
|
|
||||||
ra.IsActive);
|
|
||||||
|
|
||||||
if (existing != null)
|
|
||||||
{
|
|
||||||
// Aggiorna l'associazione esistente
|
|
||||||
existing.DestinationId = association.DestinationId;
|
|
||||||
existing.RestCredentialName = association.RestCredentialName;
|
|
||||||
existing.UpdatedAt = DateTime.UtcNow;
|
|
||||||
existing.AdditionalInfo = association.AdditionalInfo;
|
|
||||||
|
|
||||||
_context.RecordAssociations.Update(existing);
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
|
|
||||||
_logger.LogInformation("Associazione aggiornata: {SourceName}/{SourceKey} -> {DestinationEntity}/{DestinationId}",
|
|
||||||
association.SourceName, association.SourceKey, association.DestinationEntity, association.DestinationId);
|
|
||||||
|
|
||||||
return existing.Id;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Crea nuova associazione
|
|
||||||
_context.RecordAssociations.Add(association);
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
|
|
||||||
_logger.LogInformation("Nuova associazione creata: {SourceName}/{SourceKey} -> {DestinationEntity}/{DestinationId}",
|
|
||||||
association.SourceName, association.SourceKey, association.DestinationEntity, association.DestinationId);
|
|
||||||
|
|
||||||
return association.Id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Errore nel salvare l'associazione: {SourceName}/{SourceKey} -> {DestinationEntity}",
|
|
||||||
association.SourceName, association.SourceKey, association.DestinationEntity);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<RecordAssociation?> FindAssociationAsync(string sourceName, string sourceKey, string destinationEntity)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await _context.RecordAssociations
|
|
||||||
.FirstOrDefaultAsync(ra =>
|
|
||||||
ra.SourceName == sourceName &&
|
|
||||||
ra.SourceKey == sourceKey &&
|
|
||||||
ra.DestinationEntity == destinationEntity &&
|
|
||||||
ra.IsActive);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Errore nella ricerca dell'associazione: {SourceName}/{SourceKey} -> {DestinationEntity}",
|
|
||||||
sourceName, sourceKey, destinationEntity);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<RecordAssociation>> GetAssociationsBySourceAsync(string sourceName, string sourceType)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await _context.RecordAssociations
|
|
||||||
.Where(ra => ra.SourceName == sourceName && ra.SourceType == sourceType && ra.IsActive)
|
|
||||||
.OrderByDescending(ra => ra.CreatedAt)
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Errore nel recupero delle associazioni per sorgente: {SourceName} ({SourceType})",
|
|
||||||
sourceName, sourceType);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<RecordAssociation>> GetAssociationsByDestinationAsync(string destinationEntity, string restCredentialName)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await _context.RecordAssociations
|
|
||||||
.Where(ra => ra.DestinationEntity == destinationEntity &&
|
|
||||||
ra.RestCredentialName == restCredentialName &&
|
|
||||||
ra.IsActive)
|
|
||||||
.OrderByDescending(ra => ra.CreatedAt)
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Errore nel recupero delle associazioni per destinazione: {DestinationEntity} ({RestCredentialName})",
|
|
||||||
destinationEntity, restCredentialName);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<RecordAssociation>> GetAllActiveAssociationsAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await _context.RecordAssociations
|
|
||||||
.Where(ra => ra.IsActive)
|
|
||||||
.OrderByDescending(ra => ra.CreatedAt)
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Errore nel recupero di tutte le associazioni attive");
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> UpdateAssociationAsync(RecordAssociation association)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var existing = await _context.RecordAssociations.FindAsync(association.Id);
|
|
||||||
if (existing == null)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Associazione con ID {Id} non trovata per l'aggiornamento", association.Id);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
existing.DestinationId = association.DestinationId;
|
|
||||||
existing.RestCredentialName = association.RestCredentialName;
|
|
||||||
existing.UpdatedAt = DateTime.UtcNow;
|
|
||||||
existing.AdditionalInfo = association.AdditionalInfo;
|
|
||||||
existing.IsActive = association.IsActive;
|
|
||||||
|
|
||||||
_context.RecordAssociations.Update(existing);
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
|
|
||||||
_logger.LogInformation("Associazione aggiornata: ID {Id}", association.Id);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Errore nell'aggiornamento dell'associazione: ID {Id}", association.Id);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> DeactivateAssociationAsync(int id)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var association = await _context.RecordAssociations.FindAsync(id);
|
|
||||||
if (association == null)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Associazione con ID {Id} non trovata per la disattivazione", id);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
association.IsActive = false;
|
|
||||||
association.UpdatedAt = DateTime.UtcNow;
|
|
||||||
|
|
||||||
_context.RecordAssociations.Update(association);
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
|
|
||||||
_logger.LogInformation("Associazione disattivata: ID {Id}", id);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Errore nella disattivazione dell'associazione: ID {Id}", id);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> DeleteAssociationAsync(int id)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var association = await _context.RecordAssociations.FindAsync(id);
|
|
||||||
if (association == null)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Associazione con ID {Id} non trovata per l'eliminazione", id);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_context.RecordAssociations.Remove(association);
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
|
|
||||||
_logger.LogInformation("Associazione eliminata: ID {Id}", id);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Errore nell'eliminazione dell'associazione: ID {Id}", id);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<int> CleanupOldAssociationsAsync(TimeSpan olderThan)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var cutoffDate = DateTime.UtcNow - olderThan;
|
|
||||||
var oldAssociations = await _context.RecordAssociations
|
|
||||||
.Where(ra => ra.CreatedAt < cutoffDate && !ra.IsActive)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
if (oldAssociations.Any())
|
|
||||||
{
|
|
||||||
_context.RecordAssociations.RemoveRange(oldAssociations);
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
|
|
||||||
_logger.LogInformation("Pulite {Count} associazioni obsolete più vecchie di {Cutoff}",
|
|
||||||
oldAssociations.Count, cutoffDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
return oldAssociations.Count;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Errore nella pulizia delle associazioni obsolete");
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<int> ClearAssociationsAsync(string sourceName, string destinationEntity, string restCredentialName)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var associationsToDelete = await _context.RecordAssociations
|
|
||||||
.Where(ra => ra.SourceName == sourceName &&
|
|
||||||
ra.DestinationEntity == destinationEntity &&
|
|
||||||
ra.RestCredentialName == restCredentialName)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
if (associationsToDelete.Any())
|
|
||||||
{
|
|
||||||
_context.RecordAssociations.RemoveRange(associationsToDelete);
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
|
|
||||||
_logger.LogInformation("Eliminate {Count} associazioni per {SourceName} -> {DestinationEntity}",
|
|
||||||
associationsToDelete.Count, sourceName, destinationEntity);
|
|
||||||
}
|
|
||||||
|
|
||||||
return associationsToDelete.Count;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Errore nella cancellazione delle associazioni per {SourceName} -> {DestinationEntity}",
|
|
||||||
sourceName, destinationEntity);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<int> ClearAllAssociationsAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var allAssociations = await _context.RecordAssociations.ToListAsync();
|
|
||||||
var count = allAssociations.Count;
|
|
||||||
|
|
||||||
if (allAssociations.Any())
|
|
||||||
{
|
|
||||||
_context.RecordAssociations.RemoveRange(allAssociations);
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
|
|
||||||
_logger.LogWarning("Eliminate TUTTE le {Count} associazioni dal sistema", count);
|
|
||||||
}
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Errore nella cancellazione di tutte le associazioni");
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> ValidateDestinationIdAsync(string destinationId, string destinationEntity, string restCredentialName)
|
|
||||||
{
|
|
||||||
// Questa implementazione base restituisce sempre true
|
|
||||||
// Dovrebbe essere estesa per verificare effettivamente l'esistenza nel sistema REST
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// TODO: Implementare la logica di validazione effettiva con il servizio REST
|
|
||||||
// Per ora assumiamo che l'ID sia valido
|
|
||||||
_logger.LogDebug("Validazione ID destinazione {DestinationId} per entità {DestinationEntity} - Non implementata",
|
|
||||||
destinationId, destinationEntity);
|
|
||||||
|
|
||||||
return await Task.FromResult(true);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Errore nella validazione dell'ID destinazione {DestinationId}", destinationId);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<RecordAssociation>> GetInvalidAssociationsAsync(string destinationEntity, string restCredentialName)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var associations = await _context.RecordAssociations
|
|
||||||
.Where(ra => ra.DestinationEntity == destinationEntity &&
|
|
||||||
ra.RestCredentialName == restCredentialName &&
|
|
||||||
ra.IsActive)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
var invalidAssociations = new List<RecordAssociation>();
|
|
||||||
|
|
||||||
// Verifica ogni associazione
|
|
||||||
foreach (var association in associations)
|
|
||||||
{
|
|
||||||
var isValid = await ValidateDestinationIdAsync(association.DestinationId, destinationEntity, restCredentialName);
|
|
||||||
if (!isValid)
|
|
||||||
{
|
|
||||||
invalidAssociations.Add(association);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Trovate {Invalid}/{Total} associazioni non valide per {DestinationEntity}",
|
|
||||||
invalidAssociations.Count, associations.Count, destinationEntity);
|
|
||||||
|
|
||||||
return invalidAssociations;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Errore nel recupero delle associazioni non valide per {DestinationEntity}", destinationEntity);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<int> CleanupInvalidAssociationsAsync(string destinationEntity, string restCredentialName)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var invalidAssociations = await GetInvalidAssociationsAsync(destinationEntity, restCredentialName);
|
|
||||||
|
|
||||||
if (invalidAssociations.Any())
|
|
||||||
{
|
|
||||||
_context.RecordAssociations.RemoveRange(invalidAssociations);
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
|
|
||||||
_logger.LogWarning("Eliminate {Count} associazioni non valide per {DestinationEntity}",
|
|
||||||
invalidAssociations.Count, destinationEntity);
|
|
||||||
}
|
|
||||||
|
|
||||||
return invalidAssociations.Count;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Errore nella pulizia delle associazioni non valide per {DestinationEntity}", destinationEntity);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+17
-13
@@ -1,4 +1,5 @@
|
|||||||
using CredentialManager.Models;
|
using CredentialManager.Models;
|
||||||
|
using CredentialManager.Services;
|
||||||
|
|
||||||
namespace DataConnection.CredentialManagement.Interfaces;
|
namespace DataConnection.CredentialManagement.Interfaces;
|
||||||
|
|
||||||
@@ -57,17 +58,20 @@ public interface IDataConnectionCredentialService
|
|||||||
Task<(bool Success, string Message)> TestSalesforceConnectionAsync(string credentialName);
|
Task<(bool Success, string Message)> TestSalesforceConnectionAsync(string credentialName);
|
||||||
Task<(bool Success, string Message)> TestSalesforceConnectionAsync(SalesforceCredential credential);
|
Task<(bool Success, string Message)> TestSalesforceConnectionAsync(SalesforceCredential credential);
|
||||||
|
|
||||||
// Record associations
|
// Key associations
|
||||||
Task<int> SaveRecordAssociationAsync(RecordAssociation association);
|
Task<int> SaveKeyAssociationAsync(KeyAssociation association);
|
||||||
Task<RecordAssociation?> FindRecordAssociationAsync(string sourceName, string sourceKey, string destinationEntity);
|
Task<KeyAssociation?> FindKeyAssociationByValueAsync(string keyValue, string destinationEntity, string restCredentialName);
|
||||||
Task<List<RecordAssociation>> GetRecordAssociationsBySourceAsync(string sourceName, string sourceType);
|
Task<KeyAssociation?> FindKeyAssociationByValueAsync(string keyValue);
|
||||||
Task<List<RecordAssociation>> GetRecordAssociationsByDestinationAsync(string destinationEntity, string restCredentialName);
|
Task<List<KeyAssociation>> GetKeyAssociationsByDestinationAsync(string destinationEntity, string restCredentialName);
|
||||||
Task<List<RecordAssociation>> GetAllActiveRecordAssociationsAsync();
|
Task<List<KeyAssociation>> GetAllActiveKeyAssociationsAsync();
|
||||||
Task<bool> UpdateRecordAssociationAsync(RecordAssociation association);
|
Task<List<KeyAssociation>> GetAllKeyAssociationsAsync();
|
||||||
Task<bool> DeactivateRecordAssociationAsync(int id);
|
Task<bool> UpdateKeyAssociationAsync(KeyAssociation association);
|
||||||
Task<bool> DeleteRecordAssociationAsync(int id);
|
Task<bool> DeactivateKeyAssociationAsync(int id);
|
||||||
Task<int> ClearRecordAssociationsAsync(string sourceName, string destinationEntity, string restCredentialName);
|
Task<bool> DeleteKeyAssociationAsync(int id);
|
||||||
Task<int> ClearAllRecordAssociationsAsync();
|
Task<int> ClearKeyAssociationsAsync(string destinationEntity, string restCredentialName);
|
||||||
Task<List<RecordAssociation>> GetInvalidRecordAssociationsAsync(string destinationEntity, string restCredentialName);
|
Task<int> ClearAllKeyAssociationsAsync();
|
||||||
Task<int> CleanupInvalidRecordAssociationsAsync(string destinationEntity, string restCredentialName);
|
Task<List<KeyAssociation>> GetInvalidKeyAssociationsAsync(string destinationEntity, string restCredentialName);
|
||||||
|
Task<int> CleanupInvalidKeyAssociationsAsync(string destinationEntity, string restCredentialName);
|
||||||
|
Task<bool> UpdateKeyAssociationLastVerifiedAsync(int id);
|
||||||
|
Task<AssociationStatistics> GetKeyAssociationStatisticsAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ public static class ServiceCollectionExtensions
|
|||||||
// Aggiungi i servizi base di CredentialManager
|
// Aggiungi i servizi base di CredentialManager
|
||||||
services.AddCredentialManager(databasePath);
|
services.AddCredentialManager(databasePath);
|
||||||
|
|
||||||
// Aggiungi il servizio di gestione associazioni record
|
// Aggiungi il servizio di gestione associazioni per chiavi
|
||||||
services.AddScoped<IRecordAssociationService, RecordAssociationService>();
|
services.AddScoped<IKeyAssociationService, KeyAssociationService>();
|
||||||
|
|
||||||
// Aggiungi il servizio di integrazione DataConnection
|
// Aggiungi il servizio di integrazione DataConnection
|
||||||
services.AddScoped<IDataConnectionCredentialService, DataConnectionCredentialService>();
|
services.AddScoped<IDataConnectionCredentialService, DataConnectionCredentialService>();
|
||||||
|
|||||||
@@ -15,16 +15,16 @@ namespace DataConnection.CredentialManagement.Services;
|
|||||||
public class DataConnectionCredentialService : IDataConnectionCredentialService
|
public class DataConnectionCredentialService : IDataConnectionCredentialService
|
||||||
{
|
{
|
||||||
private readonly ICredentialService _credentialService;
|
private readonly ICredentialService _credentialService;
|
||||||
private readonly IRecordAssociationService _recordAssociationService;
|
private readonly IKeyAssociationService _keyAssociationService;
|
||||||
private readonly ILogger<DataConnectionCredentialService> _logger;
|
private readonly ILogger<DataConnectionCredentialService> _logger;
|
||||||
|
|
||||||
public DataConnectionCredentialService(
|
public DataConnectionCredentialService(
|
||||||
ICredentialService credentialService,
|
ICredentialService credentialService,
|
||||||
IRecordAssociationService recordAssociationService,
|
IKeyAssociationService keyAssociationService,
|
||||||
ILogger<DataConnectionCredentialService> logger)
|
ILogger<DataConnectionCredentialService> logger)
|
||||||
{
|
{
|
||||||
_credentialService = credentialService;
|
_credentialService = credentialService;
|
||||||
_recordAssociationService = recordAssociationService;
|
_keyAssociationService = keyAssociationService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -859,66 +859,81 @@ public class DataConnectionCredentialService : IDataConnectionCredentialService
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Record Associations
|
#region Key Associations
|
||||||
|
|
||||||
public async Task<int> SaveRecordAssociationAsync(RecordAssociation association)
|
public async Task<int> SaveKeyAssociationAsync(KeyAssociation association)
|
||||||
{
|
{
|
||||||
return await _recordAssociationService.SaveAssociationAsync(association);
|
return await _keyAssociationService.SaveAssociationAsync(association);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<RecordAssociation?> FindRecordAssociationAsync(string sourceName, string sourceKey, string destinationEntity)
|
public async Task<KeyAssociation?> FindKeyAssociationByValueAsync(string keyValue, string destinationEntity, string restCredentialName)
|
||||||
{
|
{
|
||||||
return await _recordAssociationService.FindAssociationAsync(sourceName, sourceKey, destinationEntity);
|
return await _keyAssociationService.FindAssociationByKeyValueAsync(keyValue, destinationEntity, restCredentialName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<RecordAssociation>> GetRecordAssociationsBySourceAsync(string sourceName, string sourceType)
|
public async Task<KeyAssociation?> FindKeyAssociationByValueAsync(string keyValue)
|
||||||
{
|
{
|
||||||
return await _recordAssociationService.GetAssociationsBySourceAsync(sourceName, sourceType);
|
return await _keyAssociationService.FindAssociationByKeyValueAsync(keyValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<RecordAssociation>> GetRecordAssociationsByDestinationAsync(string destinationEntity, string restCredentialName)
|
public async Task<List<KeyAssociation>> GetKeyAssociationsByDestinationAsync(string destinationEntity, string restCredentialName)
|
||||||
{
|
{
|
||||||
return await _recordAssociationService.GetAssociationsByDestinationAsync(destinationEntity, restCredentialName);
|
return await _keyAssociationService.GetAssociationsByDestinationAsync(destinationEntity, restCredentialName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<RecordAssociation>> GetAllActiveRecordAssociationsAsync()
|
public async Task<List<KeyAssociation>> GetAllActiveKeyAssociationsAsync()
|
||||||
{
|
{
|
||||||
return await _recordAssociationService.GetAllActiveAssociationsAsync();
|
return await _keyAssociationService.GetAllActiveAssociationsAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> UpdateRecordAssociationAsync(RecordAssociation association)
|
public async Task<List<KeyAssociation>> GetAllKeyAssociationsAsync()
|
||||||
{
|
{
|
||||||
return await _recordAssociationService.UpdateAssociationAsync(association);
|
return await _keyAssociationService.GetAllAssociationsAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> DeactivateRecordAssociationAsync(int id)
|
public async Task<bool> UpdateKeyAssociationAsync(KeyAssociation association)
|
||||||
{
|
{
|
||||||
return await _recordAssociationService.DeactivateAssociationAsync(id);
|
return await _keyAssociationService.UpdateAssociationAsync(association);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> DeleteRecordAssociationAsync(int id)
|
public async Task<bool> DeactivateKeyAssociationAsync(int id)
|
||||||
{
|
{
|
||||||
return await _recordAssociationService.DeleteAssociationAsync(id);
|
return await _keyAssociationService.DeactivateAssociationAsync(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<int> ClearRecordAssociationsAsync(string sourceName, string destinationEntity, string restCredentialName)
|
public async Task<bool> DeleteKeyAssociationAsync(int id)
|
||||||
{
|
{
|
||||||
return await _recordAssociationService.ClearAssociationsAsync(sourceName, destinationEntity, restCredentialName);
|
return await _keyAssociationService.DeleteAssociationAsync(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<int> ClearAllRecordAssociationsAsync()
|
public async Task<int> ClearKeyAssociationsAsync(string destinationEntity, string restCredentialName)
|
||||||
{
|
{
|
||||||
return await _recordAssociationService.ClearAllAssociationsAsync();
|
return await _keyAssociationService.ClearAssociationsAsync(destinationEntity, restCredentialName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<RecordAssociation>> GetInvalidRecordAssociationsAsync(string destinationEntity, string restCredentialName)
|
public async Task<int> ClearAllKeyAssociationsAsync()
|
||||||
{
|
{
|
||||||
return await _recordAssociationService.GetInvalidAssociationsAsync(destinationEntity, restCredentialName);
|
return await _keyAssociationService.ClearAllAssociationsAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<int> CleanupInvalidRecordAssociationsAsync(string destinationEntity, string restCredentialName)
|
public async Task<List<KeyAssociation>> GetInvalidKeyAssociationsAsync(string destinationEntity, string restCredentialName)
|
||||||
{
|
{
|
||||||
return await _recordAssociationService.CleanupInvalidAssociationsAsync(destinationEntity, restCredentialName);
|
return await _keyAssociationService.GetInvalidAssociationsAsync(destinationEntity, restCredentialName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> CleanupInvalidKeyAssociationsAsync(string destinationEntity, string restCredentialName)
|
||||||
|
{
|
||||||
|
return await _keyAssociationService.CleanupInvalidAssociationsAsync(destinationEntity, restCredentialName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UpdateKeyAssociationLastVerifiedAsync(int id)
|
||||||
|
{
|
||||||
|
return await _keyAssociationService.UpdateLastVerifiedAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AssociationStatistics> GetKeyAssociationStatisticsAsync()
|
||||||
|
{
|
||||||
|
return await _keyAssociationService.GetStatisticsAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -14,6 +14,10 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ExcelDataReader" Version="3.7.0" />
|
<PackageReference Include="ExcelDataReader" Version="3.7.0" />
|
||||||
<PackageReference Include="ExcelDataReader.DataSet" Version="3.7.0" />
|
<PackageReference Include="ExcelDataReader.DataSet" Version="3.7.0" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.6" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.6" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -834,13 +834,24 @@
|
|||||||
<input class="form-check-input" type="checkbox" id="useAssociations"
|
<input class="form-check-input" type="checkbox" id="useAssociations"
|
||||||
@bind="useRecordAssociations" />
|
@bind="useRecordAssociations" />
|
||||||
<label class="form-check-label" for="useAssociations">
|
<label class="form-check-label" for="useAssociations">
|
||||||
<strong>Utilizza sistema di associazioni per tracking automatico degli aggiornamenti</strong>
|
<strong>Utilizza sistema di associazioni basato sui valori delle chiavi</strong>
|
||||||
<br><small class="text-muted">Raccomandato: il sistema manterrà traccia delle associazioni tra record sorgente e destinazione</small>
|
<br><small class="text-muted">Raccomandato: il sistema manterrà traccia delle associazioni tra valori chiave e record di destinazione, permettendo aggiornamenti indipendentemente dalla sorgente</small>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (useRecordAssociations)
|
@if (useRecordAssociations)
|
||||||
{
|
{
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="fas fa-lightbulb"></i>
|
||||||
|
<strong>Come funziona il nuovo sistema:</strong>
|
||||||
|
<ul class="mb-0 mt-2">
|
||||||
|
<li>Ogni valore di chiave univoco viene associato a un record di destinazione</li>
|
||||||
|
<li>Più sorgenti diverse possono gestire lo stesso oggetto business usando lo stesso valore chiave</li>
|
||||||
|
<li>Gli aggiornamenti avvengono automaticamente quando si trova un'associazione esistente</li>
|
||||||
|
<li>Il sistema individua automaticamente le chiavi dove possibile, ma puoi sempre scegliere manualmente</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label">Campo Chiave Sorgente: <span class="text-danger">*</span></label>
|
<label class="form-label">Campo Chiave Sorgente: <span class="text-danger">*</span></label>
|
||||||
@@ -2126,79 +2137,101 @@
|
|||||||
|
|
||||||
// Genera la chiave sorgente per questo record
|
// Genera la chiave sorgente per questo record
|
||||||
var sourceKey = GenerateSourceKey(record);
|
var sourceKey = GenerateSourceKey(record);
|
||||||
var currentSourceName = selectedSourceType == "database"
|
|
||||||
? (useCustomQuery ? "custom_query" : selectedTable)
|
|
||||||
: selectedSheet;
|
|
||||||
|
|
||||||
// NUOVA LOGICA: Cerca associazione esistente
|
// NUOVO SISTEMA: Cerca associazione esistente basata sul valore della chiave
|
||||||
if (useRecordAssociations && !string.IsNullOrEmpty(sourceKey))
|
if (useRecordAssociations && !string.IsNullOrEmpty(sourceKey))
|
||||||
{
|
{
|
||||||
var existingAssociation = await CredentialService.FindRecordAssociationAsync(
|
Logger.LogInformation("ASSOCIATION DEBUG: Cerco associazione - KeyValue: '{KeyValue}', Entity: '{Entity}', Credential: '{Credential}'",
|
||||||
currentSourceName, sourceKey, selectedRestEntity.Name);
|
sourceKey, selectedRestEntity.Name, selectedRestCredential);
|
||||||
|
|
||||||
|
// Cerca se esiste già un'associazione per questo valore chiave
|
||||||
|
var existingAssociation = await CredentialService.FindKeyAssociationByValueAsync(
|
||||||
|
sourceKey, selectedRestEntity.Name, selectedRestCredential);
|
||||||
|
|
||||||
|
// FALLBACK: Se non troviamo l'associazione con tutti i parametri, proviamo solo con il KeyValue
|
||||||
|
if (existingAssociation == null)
|
||||||
|
{
|
||||||
|
Logger.LogWarning("ASSOCIATION DEBUG: Associazione non trovata con parametri specifici, provo solo con KeyValue: '{KeyValue}'", sourceKey);
|
||||||
|
existingAssociation = await CredentialService.FindKeyAssociationByValueAsync(sourceKey);
|
||||||
|
|
||||||
|
if (existingAssociation != null)
|
||||||
|
{
|
||||||
|
Logger.LogWarning("ASSOCIATION DEBUG: Trovata associazione con fallback - ID: {AssociationId}, Entity: '{Entity}', Credential: '{Credential}'",
|
||||||
|
existingAssociation.Id, existingAssociation.DestinationEntity, existingAssociation.RestCredentialName);
|
||||||
|
|
||||||
|
// Verifica se l'associazione trovata è compatibile
|
||||||
|
if (existingAssociation.DestinationEntity != selectedRestEntity.Name ||
|
||||||
|
existingAssociation.RestCredentialName != selectedRestCredential)
|
||||||
|
{
|
||||||
|
Logger.LogWarning("ASSOCIATION DEBUG: Associazione non compatibile - Entity: '{FoundEntity}' vs '{ExpectedEntity}', Credential: '{FoundCredential}' vs '{ExpectedCredential}'",
|
||||||
|
existingAssociation.DestinationEntity, selectedRestEntity.Name, existingAssociation.RestCredentialName, selectedRestCredential);
|
||||||
|
existingAssociation = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogInformation("ASSOCIATION DEBUG: Associazione finale: {Found}. ID: {AssociationId}, DestinationId: '{DestinationId}', IsActive: {IsActive}",
|
||||||
|
existingAssociation != null, existingAssociation?.Id, existingAssociation?.DestinationId, existingAssociation?.IsActive);
|
||||||
|
|
||||||
if (existingAssociation != null && existingAssociation.IsActive)
|
if (existingAssociation != null && existingAssociation.IsActive)
|
||||||
{
|
{
|
||||||
// VALIDAZIONE: Verifica se l'ID di destinazione esiste ancora nel sistema target
|
// Prova direttamente l'aggiornamento - più efficiente che verificare prima l'esistenza
|
||||||
bool destinationExists = false;
|
Logger.LogInformation("ASSOCIATION DEBUG: Tentativo aggiornamento record esistente - DestinationId: '{DestinationId}'", existingAssociation.DestinationId);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Usa il campo ID appropriato per cercare l'entità
|
var updateResult = await currentRestClient.UpdateEntityAsync(
|
||||||
var idField = GetEntityIdField(); // Potrebbe essere "DocEntry", "id", "Id", etc.
|
selectedRestEntity.Name, existingAssociation.DestinationId, restData);
|
||||||
var searchKeys = new Dictionary<string, object> { { idField, existingAssociation.DestinationId } };
|
|
||||||
|
|
||||||
var foundEntities = await currentRestClient.FindEntitiesByKeysAsync(
|
if (updateResult != null)
|
||||||
selectedRestEntity.Name, searchKeys);
|
{
|
||||||
destinationExists = foundEntities != null && foundEntities.Any();
|
updatedCount++;
|
||||||
|
transferResult.Status = "updated";
|
||||||
|
transferResult.Message = $"Record aggiornato con successo tramite associazione (ID: {existingAssociation.DestinationId})";
|
||||||
|
transferResult.EntityId = existingAssociation.DestinationId;
|
||||||
|
|
||||||
|
// Aggiorna l'associazione con la data di ultimo aggiornamento e verifica
|
||||||
|
existingAssociation.UpdatedAt = DateTime.UtcNow;
|
||||||
|
existingAssociation.LastVerifiedAt = DateTime.UtcNow;
|
||||||
|
await CredentialService.UpdateKeyAssociationAsync(existingAssociation);
|
||||||
|
|
||||||
|
Logger.LogInformation("ASSOCIATION DEBUG: Record aggiornato con successo tramite associazione: {EntityId} per valore chiave {KeyValue}",
|
||||||
|
existingAssociation.DestinationId, sourceKey);
|
||||||
|
|
||||||
|
transferResults.Add(transferResult);
|
||||||
|
recordNumber++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Update fallito ma senza eccezione - probabilmente l'entità non esiste più
|
||||||
|
Logger.LogWarning("ASSOCIATION DEBUG: Aggiornamento fallito (result null) per associazione {AssociationId} - elimino associazione e creo nuovo record", existingAssociation.Id);
|
||||||
|
goto HandleInvalidAssociation;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception updateEx)
|
||||||
{
|
{
|
||||||
Logger.LogWarning(ex, "Errore nella verifica dell'esistenza dell'entità {EntityId} - assumo che non esista", existingAssociation.DestinationId);
|
// Update fallito con eccezione - probabilmente l'entità non esiste più
|
||||||
destinationExists = false;
|
Logger.LogWarning(updateEx, "ASSOCIATION DEBUG: Aggiornamento fallito per associazione {AssociationId} - elimino associazione e creo nuovo record", existingAssociation.Id);
|
||||||
|
goto HandleInvalidAssociation;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!destinationExists)
|
HandleInvalidAssociation:
|
||||||
|
// L'ID di destinazione non esiste più o l'update è fallito - elimina l'associazione non valida
|
||||||
|
try
|
||||||
{
|
{
|
||||||
// L'ID di destinazione non esiste più - elimina l'associazione non valida
|
await CredentialService.DeleteKeyAssociationAsync(existingAssociation.Id);
|
||||||
Logger.LogWarning("ID destinazione {DestinationId} non più valido per associazione {AssociationId} - eliminazione associazione",
|
Logger.LogInformation("ASSOCIATION DEBUG: Associazione non valida eliminata: {AssociationId}", existingAssociation.Id);
|
||||||
existingAssociation.DestinationId, existingAssociation.Id);
|
|
||||||
|
|
||||||
await CredentialService.DeleteRecordAssociationAsync(existingAssociation.Id);
|
|
||||||
|
|
||||||
transferResult.Status = "error";
|
|
||||||
transferResult.Message = $"Associazione non valida eliminata (ID destinazione {existingAssociation.DestinationId} non esiste più) - creazione nuovo record";
|
|
||||||
|
|
||||||
// Procedi con la creazione di un nuovo record
|
|
||||||
goto CreateNewRecord;
|
|
||||||
}
|
}
|
||||||
|
catch (Exception delEx)
|
||||||
// L'ID di destinazione esiste - procedi con l'aggiornamento
|
|
||||||
var updateResult = await currentRestClient.UpdateEntityAsync(
|
|
||||||
selectedRestEntity.Name, existingAssociation.DestinationId, restData);
|
|
||||||
|
|
||||||
if (updateResult != null)
|
|
||||||
{
|
{
|
||||||
updatedCount++;
|
Logger.LogWarning(delEx, "Errore nell'eliminazione dell'associazione non valida {AssociationId}", existingAssociation.Id);
|
||||||
transferResult.Status = "updated";
|
|
||||||
transferResult.Message = $"Record aggiornato con successo tramite associazione (ID: {existingAssociation.DestinationId})";
|
|
||||||
transferResult.EntityId = existingAssociation.DestinationId;
|
|
||||||
|
|
||||||
// Aggiorna l'associazione con la data di ultimo aggiornamento
|
|
||||||
existingAssociation.UpdatedAt = DateTime.UtcNow;
|
|
||||||
await CredentialService.UpdateRecordAssociationAsync(existingAssociation);
|
|
||||||
|
|
||||||
Logger.LogDebug("Record aggiornato tramite associazione: {EntityId} per chiave sorgente {SourceKey}",
|
|
||||||
existingAssociation.DestinationId, sourceKey);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Se l'aggiornamento fallisce, prova a creare un nuovo record
|
|
||||||
Logger.LogWarning("Aggiornamento fallito per associazione {AssociationId}, provo a creare nuovo record", existingAssociation.Id);
|
|
||||||
goto CreateNewRecord;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
transferResults.Add(transferResult);
|
transferResult.Status = "info";
|
||||||
recordNumber++;
|
transferResult.Message = $"Associazione non valida eliminata (aggiornamento fallito) - creazione nuovo record";
|
||||||
continue;
|
|
||||||
|
// Procedi con la creazione di un nuovo record (non aggiungere il result qui, sarà aggiunto dopo CreateNewRecord)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2212,31 +2245,41 @@
|
|||||||
transferResult.Status = "success";
|
transferResult.Status = "success";
|
||||||
transferResult.Message = "Record inserito con successo";
|
transferResult.Message = "Record inserito con successo";
|
||||||
transferResult.EntityId = result.ContainsKey("id") ? result["id"]?.ToString() :
|
transferResult.EntityId = result.ContainsKey("id") ? result["id"]?.ToString() :
|
||||||
result.ContainsKey("Id") ? result["Id"]?.ToString() : null;
|
result.ContainsKey("Id") ? result["Id"]?.ToString() :
|
||||||
|
result.ContainsKey("DocEntry") ? result["DocEntry"]?.ToString() : null;
|
||||||
|
|
||||||
// Crea associazione solo se abbiamo una chiave sorgente e un ID destinazione
|
// Crea associazione solo se abbiamo una chiave sorgente e un ID destinazione
|
||||||
if (useRecordAssociations && !string.IsNullOrEmpty(sourceKey) && !string.IsNullOrEmpty(transferResult.EntityId))
|
if (useRecordAssociations && !string.IsNullOrEmpty(sourceKey) && !string.IsNullOrEmpty(transferResult.EntityId))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var association = new RecordAssociation
|
// Determina i campi chiave automaticamente
|
||||||
|
var destinationKeyField = GetEntityIdField(); // Campo chiave nella destinazione
|
||||||
|
|
||||||
|
var association = new KeyAssociation
|
||||||
{
|
{
|
||||||
SourceName = currentSourceName,
|
KeyValue = sourceKey,
|
||||||
SourceType = selectedSourceType,
|
SourceKeyField = sourceKeyField,
|
||||||
SourceKey = sourceKey,
|
DestinationKeyField = destinationKeyField,
|
||||||
DestinationEntity = selectedRestEntity.Name,
|
DestinationEntity = selectedRestEntity.Name,
|
||||||
DestinationId = transferResult.EntityId,
|
DestinationId = transferResult.EntityId,
|
||||||
RestCredentialName = selectedRestCredential,
|
RestCredentialName = selectedRestCredential,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
LastVerifiedAt = DateTime.UtcNow,
|
||||||
AdditionalInfo = System.Text.Json.JsonSerializer.Serialize(new
|
AdditionalInfo = System.Text.Json.JsonSerializer.Serialize(new
|
||||||
{
|
{
|
||||||
TransferDate = DateTime.UtcNow,
|
TransferDate = DateTime.UtcNow,
|
||||||
RecordNumber = recordNumber,
|
RecordNumber = recordNumber,
|
||||||
MappingCount = fieldMappings.Count
|
MappingCount = fieldMappings.Count,
|
||||||
|
SourceType = selectedSourceType
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
await CredentialService.SaveRecordAssociationAsync(association);
|
Logger.LogInformation("ASSOCIATION DEBUG: Creazione nuova associazione - KeyValue: '{KeyValue}', Entity: '{Entity}', DestinationId: '{DestinationId}', Credential: '{Credential}'",
|
||||||
Logger.LogDebug("Associazione creata: {SourceKey} -> {DestinationId}", sourceKey, transferResult.EntityId);
|
sourceKey, selectedRestEntity.Name, transferResult.EntityId, selectedRestCredential);
|
||||||
|
|
||||||
|
var associationId = await CredentialService.SaveKeyAssociationAsync(association);
|
||||||
|
Logger.LogInformation("DEBUG: Associazione salvata con ID: {AssociationId}", associationId);
|
||||||
}
|
}
|
||||||
catch (Exception assocEx)
|
catch (Exception assocEx)
|
||||||
{
|
{
|
||||||
@@ -2599,7 +2642,8 @@
|
|||||||
throw new InvalidOperationException($"Il valore del campo chiave '{sourceKeyField}' è vuoto o null per questo record.");
|
throw new InvalidOperationException($"Il valore del campo chiave '{sourceKeyField}' è vuoto o null per questo record.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return keyValue;
|
// Normalizza il valore della chiave (trim e gestione case-sensitive)
|
||||||
|
return keyValue.Trim();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
+260
-403
@@ -1,27 +1,129 @@
|
|||||||
@page "/record-associations"
|
@page "/key-associations"
|
||||||
@using CredentialManager.Models
|
@using CredentialManager.Models
|
||||||
|
@using CredentialManager.Services
|
||||||
@using DataConnection.CredentialManagement.Interfaces
|
@using DataConnection.CredentialManagement.Interfaces
|
||||||
@using Microsoft.AspNetCore.Components.Forms
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
@using Microsoft.JSInterop
|
@using Microsoft.JSInterop
|
||||||
@inject IDataConnectionCredentialService CredentialService
|
@inject IDataConnectionCredentialService CredentialService
|
||||||
@inject IJSRuntime JSRuntime
|
@inject IJSRuntime JSRuntime
|
||||||
@inject ILogger<RecordAssociations> Logger
|
@inject ILogger<KeyAssociations> Logger
|
||||||
|
|
||||||
<PageTitle>Associazioni Record</PageTitle>
|
<PageTitle>Gestione Associazioni Chiavi</PageTitle>
|
||||||
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<h3><i class="fas fa-link"></i> Associazioni Record</h3>
|
<h3><i class="fas fa-key"></i> Gestione Associazioni Chiavi</h3>
|
||||||
<p class="text-muted">Visualizza e gestisci le associazioni tra record sorgente e destinazione</p>
|
<p class="text-muted">
|
||||||
|
Visualizza e gestisci le associazioni basate sui valori delle chiavi.
|
||||||
|
Ogni associazione lega un valore di chiave univoco a un record di destinazione,
|
||||||
|
indipendentemente dalla sorgente che ha generato quel valore.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Statistiche -->
|
||||||
|
@if (statistics != null)
|
||||||
|
{
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-2">
|
||||||
|
<div class="card bg-primary text-white">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<h4 class="card-title">@statistics.TotalAssociations</h4>
|
||||||
|
<p class="card-text">Totali</p>
|
||||||
|
</div>
|
||||||
|
<div class="align-self-center">
|
||||||
|
<i class="fas fa-link fa-2x"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<div class="card bg-success text-white">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<h4 class="card-title">@statistics.ActiveAssociations</h4>
|
||||||
|
<p class="card-text">Attive</p>
|
||||||
|
</div>
|
||||||
|
<div class="align-self-center">
|
||||||
|
<i class="fas fa-check-circle fa-2x"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<div class="card bg-warning text-white">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<h4 class="card-title">@statistics.InactiveAssociations</h4>
|
||||||
|
<p class="card-text">Disattive</p>
|
||||||
|
</div>
|
||||||
|
<div class="align-self-center">
|
||||||
|
<i class="fas fa-pause-circle fa-2x"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<div class="card bg-info text-white">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<h4 class="card-title">@statistics.UniqueKeyValues</h4>
|
||||||
|
<p class="card-text">Chiavi Uniche</p>
|
||||||
|
</div>
|
||||||
|
<div class="align-self-center">
|
||||||
|
<i class="fas fa-key fa-2x"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<div class="card bg-secondary text-white">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<h4 class="card-title">@statistics.UniqueDestinationEntities</h4>
|
||||||
|
<p class="card-text">Entità</p>
|
||||||
|
</div>
|
||||||
|
<div class="align-self-center">
|
||||||
|
<i class="fas fa-database fa-2x"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<div class="card bg-dark text-white">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<h6 class="card-title">@(statistics.OldestAssociation?.ToString("dd/MM/yy") ?? "N/A")</h6>
|
||||||
|
<p class="card-text">Più Vecchia</p>
|
||||||
|
</div>
|
||||||
|
<div class="align-self-center">
|
||||||
|
<i class="fas fa-calendar fa-2x"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Filtri -->
|
<!-- Filtri -->
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label">Filtra per Sorgente:</label>
|
<label class="form-label">Filtra per Valore Chiave:</label>
|
||||||
<input class="form-control" @bind="sourceFilter" @bind:event="oninput" @onkeyup="ApplyFilters" placeholder="Nome sorgente..." />
|
<input class="form-control" @bind="keyValueFilter" @bind:event="oninput" @onkeyup="ApplyFilters" placeholder="Valore chiave..." />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label">Filtra per Entità:</label>
|
<label class="form-label">Filtra per Entità:</label>
|
||||||
@@ -50,36 +152,25 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-4">
|
<div class="col-md-6">
|
||||||
<h6><i class="fas fa-broom"></i> Pulizia Associazioni</h6>
|
<h6>Operazioni di Pulizia</h6>
|
||||||
<div class="d-grid gap-2">
|
<div class="btn-group me-3">
|
||||||
<button class="btn btn-warning" @onclick="() => ShowClearConfirmation(false)" disabled="@isProcessing">
|
<button class="btn btn-warning" @onclick="ValidateAssociations" disabled="@isProcessing">
|
||||||
<i class="fas fa-trash-alt"></i> Pulisci Associazioni Filtrate
|
<i class="fas fa-check-double"></i> Valida Associazioni
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger" @onclick="() => ShowClearConfirmation(true)" disabled="@isProcessing">
|
<button class="btn btn-danger" @onclick="CleanupInvalidAssociations" disabled="@isProcessing">
|
||||||
<i class="fas fa-trash"></i> Elimina TUTTE le Associazioni
|
<i class="fas fa-broom"></i> Pulisci Non Valide
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-6">
|
||||||
<h6><i class="fas fa-check-circle"></i> Validazione Associazioni</h6>
|
<h6>Operazioni Avanzate</h6>
|
||||||
<div class="d-grid gap-2">
|
<div class="btn-group">
|
||||||
<button class="btn btn-info" @onclick="ValidateAllAssociations" disabled="@isProcessing">
|
<button class="btn btn-info" @onclick="ExportAssociations" disabled="@isProcessing">
|
||||||
<i class="fas fa-search"></i> Verifica Associazioni Non Valide
|
<i class="fas fa-download"></i> Esporta CSV
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-warning" @onclick="CleanupInvalidAssociations" disabled="@isProcessing">
|
<button class="btn btn-danger" @onclick="ClearAllAssociations" disabled="@isProcessing">
|
||||||
<i class="fas fa-broom"></i> Pulisci Associazioni Non Valide
|
<i class="fas fa-trash-alt"></i> Elimina Tutte
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<h6><i class="fas fa-download"></i> Esportazione</h6>
|
|
||||||
<div class="d-grid gap-2">
|
|
||||||
<button class="btn btn-success" @onclick="ExportToCsv" disabled="@isProcessing">
|
|
||||||
<i class="fas fa-file-csv"></i> Esporta in CSV
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-outline-primary" @onclick="ShowImportDialog" disabled="@isProcessing">
|
|
||||||
<i class="fas fa-upload"></i> Importa da CSV
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,18 +180,17 @@
|
|||||||
{
|
{
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<div class="progress">
|
<div class="progress">
|
||||||
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 100%">
|
<div class="progress-bar progress-bar-striped progress-bar-animated" style="width: 100%"></div>
|
||||||
@processingMessage
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<small class="text-muted">@processingMessage</small>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (!string.IsNullOrEmpty(operationMessage))
|
@if (!string.IsNullOrEmpty(operationMessage))
|
||||||
{
|
{
|
||||||
<div class="alert @(operationMessageType == "success" ? "alert-success" : operationMessageType == "warning" ? "alert-warning" : "alert-danger") mt-3">
|
<div class="alert alert-@operationMessageType alert-dismissible fade show mt-3" role="alert">
|
||||||
<i class="fas @(operationMessageType == "success" ? "fa-check-circle" : operationMessageType == "warning" ? "fa-exclamation-triangle" : "fa-exclamation-circle")"></i>
|
|
||||||
@operationMessage
|
@operationMessage
|
||||||
|
<button type="button" class="btn-close" @onclick="() => operationMessage = string.Empty"></button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -108,70 +198,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Statistiche -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="card bg-primary text-white">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex justify-content-between">
|
|
||||||
<div>
|
|
||||||
<h4 class="card-title">@filteredAssociations.Count</h4>
|
|
||||||
<p class="card-text">Associazioni Totali</p>
|
|
||||||
</div>
|
|
||||||
<div class="align-self-center">
|
|
||||||
<i class="fas fa-link fa-2x"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="card bg-success text-white">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex justify-content-between">
|
|
||||||
<div>
|
|
||||||
<h4 class="card-title">@filteredAssociations.Where(a => a.IsActive).Count()</h4>
|
|
||||||
<p class="card-text">Attive</p>
|
|
||||||
</div>
|
|
||||||
<div class="align-self-center">
|
|
||||||
<i class="fas fa-check-circle fa-2x"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="card bg-warning text-white">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex justify-content-between">
|
|
||||||
<div>
|
|
||||||
<h4 class="card-title">@filteredAssociations.Where(a => !a.IsActive).Count()</h4>
|
|
||||||
<p class="card-text">Disattivate</p>
|
|
||||||
</div>
|
|
||||||
<div class="align-self-center">
|
|
||||||
<i class="fas fa-pause-circle fa-2x"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="card bg-info text-white">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex justify-content-between">
|
|
||||||
<div>
|
|
||||||
<h4 class="card-title">@filteredAssociations.Select(a => a.SourceName).Distinct().Count()</h4>
|
|
||||||
<p class="card-text">Sorgenti Diverse</p>
|
|
||||||
</div>
|
|
||||||
<div class="align-self-center">
|
|
||||||
<i class="fas fa-database fa-2x"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tabella Associazioni -->
|
<!-- Tabella Associazioni -->
|
||||||
@if (isLoading)
|
@if (isLoading)
|
||||||
{
|
{
|
||||||
@@ -188,7 +214,7 @@
|
|||||||
<i class="fas fa-info-circle"></i>
|
<i class="fas fa-info-circle"></i>
|
||||||
@if (!allAssociations.Any())
|
@if (!allAssociations.Any())
|
||||||
{
|
{
|
||||||
<span>Nessuna associazione trovata. Le associazioni vengono create automaticamente durante il trasferimento dati.</span>
|
<span>Nessuna associazione trovata. Le associazioni vengono create automaticamente durante il trasferimento dati quando il sistema di associazioni è abilitato.</span>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -201,7 +227,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="mb-0">
|
<h5 class="mb-0">
|
||||||
<i class="fas fa-table"></i> Associazioni Record
|
<i class="fas fa-table"></i> Associazioni Chiavi
|
||||||
<span class="badge bg-primary ms-2">@filteredAssociations.Count</span>
|
<span class="badge bg-primary ms-2">@filteredAssociations.Count</span>
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
@@ -210,15 +236,15 @@
|
|||||||
<table class="table table-striped table-hover mb-0">
|
<table class="table table-striped table-hover mb-0">
|
||||||
<thead class="table-dark">
|
<thead class="table-dark">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Sorgente</th>
|
<th>Valore Chiave</th>
|
||||||
<th>Tipo</th>
|
<th>Campo Sorgente</th>
|
||||||
<th>Chiave Sorgente</th>
|
<th>Campo Destinazione</th>
|
||||||
<th>Entità Destinazione</th>
|
<th>Entità Destinazione</th>
|
||||||
<th>ID Destinazione</th>
|
<th>ID Destinazione</th>
|
||||||
<th>Credenziale REST</th>
|
<th>Credenziale</th>
|
||||||
<th>Stato</th>
|
<th>Stato</th>
|
||||||
<th>Creata</th>
|
<th>Creata</th>
|
||||||
<th>Aggiornata</th>
|
<th>Verificata</th>
|
||||||
<th>Azioni</th>
|
<th>Azioni</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -227,15 +253,13 @@
|
|||||||
{
|
{
|
||||||
<tr class="@(association.IsActive ? "" : "table-secondary")">
|
<tr class="@(association.IsActive ? "" : "table-secondary")">
|
||||||
<td>
|
<td>
|
||||||
<strong>@association.SourceName</strong>
|
<code class="small">@association.KeyValue</code>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge @(association.SourceType == "database" ? "bg-primary" : "bg-info")">
|
<span class="badge bg-info">@association.SourceKeyField</span>
|
||||||
@association.SourceType
|
|
||||||
</span>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<code class="small">@association.SourceKey</code>
|
<span class="badge bg-secondary">@association.DestinationKeyField</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<strong>@association.DestinationEntity</strong>
|
<strong>@association.DestinationEntity</strong>
|
||||||
@@ -267,32 +291,29 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
@(association.UpdatedAt?.ToString("dd/MM/yyyy HH:mm") ?? "-")
|
@(association.LastVerifiedAt?.ToString("dd/MM/yyyy HH:mm") ?? "Mai")
|
||||||
</small>
|
</small>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group btn-group-sm">
|
<div class="btn-group btn-group-sm">
|
||||||
@if (association.IsActive)
|
@if (association.IsActive)
|
||||||
{
|
{
|
||||||
<button class="btn btn-warning" @onclick="() => DeactivateAssociation(association.Id)" title="Disattiva">
|
<button class="btn btn-outline-warning" @onclick="() => DeactivateAssociation(association.Id)" title="Disattiva">
|
||||||
<i class="fas fa-pause"></i>
|
<i class="fas fa-pause"></i>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<button class="btn btn-success" @onclick="() => ActivateAssociation(association.Id)" title="Riattiva">
|
<button class="btn btn-outline-success" @onclick="() => ActivateAssociation(association.Id)" title="Riattiva">
|
||||||
<i class="fas fa-play"></i>
|
<i class="fas fa-play"></i>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
<button class="btn btn-danger" @onclick="() => DeleteAssociation(association.Id)" title="Elimina definitivamente">
|
<button class="btn btn-outline-info" @onclick="() => ShowAssociationDetails(association)" title="Dettagli">
|
||||||
|
<i class="fas fa-info"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-danger" @onclick="() => DeleteAssociation(association.Id)" title="Elimina">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
@if (!string.IsNullOrEmpty(association.AdditionalInfo))
|
|
||||||
{
|
|
||||||
<button class="btn btn-info" @onclick="() => ShowAdditionalInfo(association)" title="Mostra dettagli">
|
|
||||||
<i class="fas fa-info"></i>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -304,116 +325,54 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Paginazione -->
|
<!-- Paginazione -->
|
||||||
@if (filteredAssociations.Count > pageSize)
|
@if (totalPages > 1)
|
||||||
{
|
{
|
||||||
<nav class="mt-3">
|
<nav aria-label="Paginazione associazioni" class="mt-3">
|
||||||
<ul class="pagination justify-content-center">
|
<ul class="pagination justify-content-center">
|
||||||
<li class="page-item @(currentPage == 1 ? "disabled" : "")">
|
<li class="page-item @(currentPage == 1 ? "disabled" : "")">
|
||||||
<button class="page-link" @onclick="() => ChangePage(currentPage - 1)">Precedente</button>
|
<a class="page-link" @onclick="() => ChangePage(currentPage - 1)">Precedente</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
@for (int i = Math.Max(1, currentPage - 2); i <= Math.Min(totalPages, currentPage + 2); i++)
|
@for (int i = Math.Max(1, currentPage - 2); i <= Math.Min(totalPages, currentPage + 2); i++)
|
||||||
{
|
{
|
||||||
var pageNum = i;
|
<li class="page-item @(i == currentPage ? "active" : "")">
|
||||||
<li class="page-item @(currentPage == pageNum ? "active" : "")">
|
<a class="page-link" @onclick="() => ChangePage(i)">@i</a>
|
||||||
<button class="page-link" @onclick="() => ChangePage(pageNum)">@pageNum</button>
|
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
|
|
||||||
<li class="page-item @(currentPage == totalPages ? "disabled" : "")">
|
<li class="page-item @(currentPage == totalPages ? "disabled" : "")">
|
||||||
<button class="page-link" @onclick="() => ChangePage(currentPage + 1)">Successivo</button>
|
<a class="page-link" @onclick="() => ChangePage(currentPage + 1)">Successiva</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Azioni di massa -->
|
|
||||||
@if (filteredAssociations.Any())
|
|
||||||
{
|
|
||||||
<div class="row mt-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h6 class="mb-0">Azioni di Massa</h6>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="btn-group">
|
|
||||||
<button class="btn btn-warning" @onclick="DeactivateAllInactive">
|
|
||||||
<i class="fas fa-pause"></i> Disattiva Tutte Inattive
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-danger" @onclick="DeleteAllInactive">
|
|
||||||
<i class="fas fa-trash"></i> Elimina Tutte Disattivate
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-info" @onclick="ExportAssociations">
|
|
||||||
<i class="fas fa-download"></i> Esporta CSV
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal di Conferma -->
|
|
||||||
@if (showConfirmModal)
|
|
||||||
{
|
|
||||||
<div class="modal fade show d-block" style="background-color: rgba(0,0,0,0.5);">
|
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">
|
|
||||||
<i class="fas fa-exclamation-triangle text-warning"></i> Conferma Eliminazione
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p>@confirmMessage</p>
|
|
||||||
<div class="alert alert-warning">
|
|
||||||
<i class="fas fa-info-circle"></i>
|
|
||||||
<strong>Attenzione:</strong> Questa operazione eliminerà definitivamente le associazioni dal database e non potrà essere annullata.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" @onclick="CancelClearConfirmation">
|
|
||||||
<i class="fas fa-times"></i> Annulla
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-danger" @onclick="ConfirmClearAssociations">
|
|
||||||
<i class="fas fa-trash"></i> Conferma Eliminazione
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private List<RecordAssociation> allAssociations = new();
|
// Dati
|
||||||
private List<RecordAssociation> filteredAssociations = new();
|
private List<KeyAssociation> allAssociations = new();
|
||||||
private List<RecordAssociation> pagedAssociations = new();
|
private List<KeyAssociation> filteredAssociations = new();
|
||||||
private bool isLoading = true;
|
private List<KeyAssociation> pagedAssociations = new();
|
||||||
|
private AssociationStatistics? statistics;
|
||||||
|
|
||||||
// Filtri
|
// Filtri
|
||||||
private string sourceFilter = "";
|
private string keyValueFilter = "";
|
||||||
private string entityFilter = "";
|
private string entityFilter = "";
|
||||||
private string credentialFilter = "";
|
private string credentialFilter = "";
|
||||||
|
|
||||||
// Paginazione
|
// Paginazione
|
||||||
private int currentPage = 1;
|
private int currentPage = 1;
|
||||||
private int pageSize = 25;
|
private int pageSize = 25;
|
||||||
private int totalPages => (int)Math.Ceiling((double)filteredAssociations.Count / pageSize);
|
private int totalPages = 1;
|
||||||
|
|
||||||
// Gestione operazioni
|
// Stato
|
||||||
|
private bool isLoading = true;
|
||||||
private bool isProcessing = false;
|
private bool isProcessing = false;
|
||||||
private string processingMessage = "";
|
private string processingMessage = "";
|
||||||
private string operationMessage = "";
|
private string operationMessage = "";
|
||||||
private string operationMessageType = "";
|
private string operationMessageType = "";
|
||||||
|
|
||||||
// Modal di conferma
|
|
||||||
private bool showConfirmModal = false;
|
|
||||||
private bool isDeleteAll = false;
|
|
||||||
private string confirmMessage = "";
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
await RefreshAssociations();
|
await RefreshAssociations();
|
||||||
@@ -421,27 +380,32 @@
|
|||||||
|
|
||||||
private async Task RefreshAssociations()
|
private async Task RefreshAssociations()
|
||||||
{
|
{
|
||||||
|
isLoading = true;
|
||||||
|
operationMessage = "";
|
||||||
|
StateHasChanged();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
isLoading = true;
|
allAssociations = await CredentialService.GetAllKeyAssociationsAsync();
|
||||||
allAssociations = await CredentialService.GetAllActiveRecordAssociationsAsync();
|
statistics = await CredentialService.GetKeyAssociationStatisticsAsync();
|
||||||
ApplyFilters();
|
ApplyFilters();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, "Errore nel caricamento delle associazioni");
|
Logger.LogError(ex, "Errore nel caricamento delle associazioni");
|
||||||
await JSRuntime.InvokeVoidAsync("alert", $"Errore nel caricamento delle associazioni: {ex.Message}");
|
SetOperationMessage($"Errore nel caricamento: {ex.Message}", "danger");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyFilters()
|
private void ApplyFilters()
|
||||||
{
|
{
|
||||||
filteredAssociations = allAssociations.Where(a =>
|
filteredAssociations = allAssociations.Where(a =>
|
||||||
(string.IsNullOrEmpty(sourceFilter) || a.SourceName.Contains(sourceFilter, StringComparison.OrdinalIgnoreCase)) &&
|
(string.IsNullOrEmpty(keyValueFilter) || a.KeyValue.Contains(keyValueFilter, StringComparison.OrdinalIgnoreCase)) &&
|
||||||
(string.IsNullOrEmpty(entityFilter) || a.DestinationEntity.Contains(entityFilter, StringComparison.OrdinalIgnoreCase)) &&
|
(string.IsNullOrEmpty(entityFilter) || a.DestinationEntity.Contains(entityFilter, StringComparison.OrdinalIgnoreCase)) &&
|
||||||
(string.IsNullOrEmpty(credentialFilter) || a.RestCredentialName.Contains(credentialFilter, StringComparison.OrdinalIgnoreCase))
|
(string.IsNullOrEmpty(credentialFilter) || a.RestCredentialName.Contains(credentialFilter, StringComparison.OrdinalIgnoreCase))
|
||||||
).OrderByDescending(a => a.CreatedAt).ToList();
|
).OrderByDescending(a => a.CreatedAt).ToList();
|
||||||
@@ -453,7 +417,7 @@
|
|||||||
|
|
||||||
private void ClearFilters()
|
private void ClearFilters()
|
||||||
{
|
{
|
||||||
sourceFilter = "";
|
keyValueFilter = "";
|
||||||
entityFilter = "";
|
entityFilter = "";
|
||||||
credentialFilter = "";
|
credentialFilter = "";
|
||||||
ApplyFilters();
|
ApplyFilters();
|
||||||
@@ -470,6 +434,7 @@
|
|||||||
|
|
||||||
private void UpdatePagedAssociations()
|
private void UpdatePagedAssociations()
|
||||||
{
|
{
|
||||||
|
totalPages = (int)Math.Ceiling((double)filteredAssociations.Count / pageSize);
|
||||||
var startIndex = (currentPage - 1) * pageSize;
|
var startIndex = (currentPage - 1) * pageSize;
|
||||||
pagedAssociations = filteredAssociations.Skip(startIndex).Take(pageSize).ToList();
|
pagedAssociations = filteredAssociations.Skip(startIndex).Take(pageSize).ToList();
|
||||||
}
|
}
|
||||||
@@ -480,21 +445,21 @@
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var success = await CredentialService.DeactivateRecordAssociationAsync(id);
|
var success = await CredentialService.DeactivateKeyAssociationAsync(id);
|
||||||
if (success)
|
if (success)
|
||||||
{
|
{
|
||||||
await JSRuntime.InvokeVoidAsync("alert", "Associazione disattivata con successo!");
|
SetOperationMessage("Associazione disattivata con successo!", "success");
|
||||||
await RefreshAssociations();
|
await RefreshAssociations();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await JSRuntime.InvokeVoidAsync("alert", "Errore nella disattivazione dell'associazione.");
|
SetOperationMessage("Errore nella disattivazione dell'associazione.", "danger");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, "Errore nella disattivazione dell'associazione {Id}", id);
|
Logger.LogError(ex, "Errore nella disattivazione dell'associazione {Id}", id);
|
||||||
await JSRuntime.InvokeVoidAsync("alert", $"Errore: {ex.Message}");
|
SetOperationMessage($"Errore: {ex.Message}", "danger");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -507,214 +472,79 @@
|
|||||||
if (association != null)
|
if (association != null)
|
||||||
{
|
{
|
||||||
association.IsActive = true;
|
association.IsActive = true;
|
||||||
var success = await CredentialService.UpdateRecordAssociationAsync(association);
|
association.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
var success = await CredentialService.UpdateKeyAssociationAsync(association);
|
||||||
if (success)
|
if (success)
|
||||||
{
|
{
|
||||||
await JSRuntime.InvokeVoidAsync("alert", "Associazione riattivata con successo!");
|
SetOperationMessage("Associazione riattivata con successo!", "success");
|
||||||
await RefreshAssociations();
|
await RefreshAssociations();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await JSRuntime.InvokeVoidAsync("alert", "Errore nella riattivazione dell'associazione.");
|
SetOperationMessage("Errore nella riattivazione dell'associazione.", "danger");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, "Errore nella riattivazione dell'associazione {Id}", id);
|
Logger.LogError(ex, "Errore nella riattivazione dell'associazione {Id}", id);
|
||||||
await JSRuntime.InvokeVoidAsync("alert", $"Errore: {ex.Message}");
|
SetOperationMessage($"Errore: {ex.Message}", "danger");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task DeleteAssociation(int id)
|
private async Task DeleteAssociation(int id)
|
||||||
{
|
{
|
||||||
if (await JSRuntime.InvokeAsync<bool>("confirm", "Sei sicuro di voler eliminare definitivamente questa associazione? Questa azione non può essere annullata."))
|
if (await JSRuntime.InvokeAsync<bool>("confirm", "Sei sicuro di voler eliminare questa associazione? Questa operazione non può essere annullata."))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var success = await CredentialService.DeleteRecordAssociationAsync(id);
|
var success = await CredentialService.DeleteKeyAssociationAsync(id);
|
||||||
if (success)
|
if (success)
|
||||||
{
|
{
|
||||||
await JSRuntime.InvokeVoidAsync("alert", "Associazione eliminata con successo!");
|
SetOperationMessage("Associazione eliminata con successo!", "success");
|
||||||
await RefreshAssociations();
|
await RefreshAssociations();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await JSRuntime.InvokeVoidAsync("alert", "Errore nell'eliminazione dell'associazione.");
|
SetOperationMessage("Errore nell'eliminazione dell'associazione.", "danger");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, "Errore nell'eliminazione dell'associazione {Id}", id);
|
Logger.LogError(ex, "Errore nell'eliminazione dell'associazione {Id}", id);
|
||||||
await JSRuntime.InvokeVoidAsync("alert", $"Errore: {ex.Message}");
|
SetOperationMessage($"Errore: {ex.Message}", "danger");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ShowAdditionalInfo(RecordAssociation association)
|
private async Task ShowAssociationDetails(KeyAssociation association)
|
||||||
{
|
{
|
||||||
var info = $"Informazioni aggiuntive per l'associazione:\n\n";
|
var info = $"Dettagli associazione:\n\n";
|
||||||
info += $"ID: {association.Id}\n";
|
info += $"ID: {association.Id}\n";
|
||||||
info += $"Sorgente: {association.SourceName} ({association.SourceType})\n";
|
info += $"Valore Chiave: {association.KeyValue}\n";
|
||||||
info += $"Chiave Sorgente: {association.SourceKey}\n";
|
info += $"Campo Sorgente: {association.SourceKeyField}\n";
|
||||||
info += $"Destinazione: {association.DestinationEntity}\n";
|
info += $"Campo Destinazione: {association.DestinationKeyField}\n";
|
||||||
|
info += $"Entità: {association.DestinationEntity}\n";
|
||||||
info += $"ID Destinazione: {association.DestinationId}\n";
|
info += $"ID Destinazione: {association.DestinationId}\n";
|
||||||
info += $"Credenziale REST: {association.RestCredentialName}\n";
|
info += $"Credenziale: {association.RestCredentialName}\n";
|
||||||
info += $"Creata: {association.CreatedAt}\n";
|
info += $"Creata: {association.CreatedAt:dd/MM/yyyy HH:mm}\n";
|
||||||
if (association.UpdatedAt.HasValue)
|
if (association.UpdatedAt.HasValue)
|
||||||
info += $"Aggiornata: {association.UpdatedAt}\n";
|
info += $"Aggiornata: {association.UpdatedAt:dd/MM/yyyy HH:mm}\n";
|
||||||
|
if (association.LastVerifiedAt.HasValue)
|
||||||
|
info += $"Verificata: {association.LastVerifiedAt:dd/MM/yyyy HH:mm}\n";
|
||||||
info += $"Stato: {(association.IsActive ? "Attiva" : "Disattivata")}\n";
|
info += $"Stato: {(association.IsActive ? "Attiva" : "Disattivata")}\n";
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(association.SourcesInfo))
|
||||||
|
info += $"\nSorgenti:\n{association.SourcesInfo}\n";
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(association.AdditionalInfo))
|
if (!string.IsNullOrEmpty(association.AdditionalInfo))
|
||||||
info += $"\nInformazioni aggiuntive:\n{association.AdditionalInfo}";
|
info += $"\nInformazioni aggiuntive:\n{association.AdditionalInfo}";
|
||||||
|
|
||||||
await JSRuntime.InvokeVoidAsync("alert", info);
|
await JSRuntime.InvokeVoidAsync("alert", info);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task DeactivateAllInactive()
|
private async Task ValidateAssociations()
|
||||||
{
|
|
||||||
if (await JSRuntime.InvokeAsync<bool>("confirm", "Sei sicuro di voler disattivare tutte le associazioni che non sono attualmente in uso?"))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Implementa logica per disattivare associazioni inattive
|
|
||||||
await JSRuntime.InvokeVoidAsync("alert", "Funzionalità in via di sviluppo.");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogError(ex, "Errore nella disattivazione di massa");
|
|
||||||
await JSRuntime.InvokeVoidAsync("alert", $"Errore: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DeleteAllInactive()
|
|
||||||
{
|
|
||||||
if (await JSRuntime.InvokeAsync<bool>("confirm", "Sei sicuro di voler eliminare definitivamente tutte le associazioni disattivate? Questa azione non può essere annullata."))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var inactiveAssociations = allAssociations.Where(a => !a.IsActive).ToList();
|
|
||||||
var deletedCount = 0;
|
|
||||||
|
|
||||||
foreach (var association in inactiveAssociations)
|
|
||||||
{
|
|
||||||
if (await CredentialService.DeleteRecordAssociationAsync(association.Id))
|
|
||||||
{
|
|
||||||
deletedCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await JSRuntime.InvokeVoidAsync("alert", $"Eliminate {deletedCount} associazioni disattivate.");
|
|
||||||
await RefreshAssociations();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogError(ex, "Errore nell'eliminazione di massa");
|
|
||||||
await JSRuntime.InvokeVoidAsync("alert", $"Errore: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ExportAssociations()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var csv = "Sorgente,Tipo,Chiave Sorgente,Entità Destinazione,ID Destinazione,Credenziale REST,Stato,Creata,Aggiornata\n";
|
|
||||||
|
|
||||||
foreach (var association in filteredAssociations)
|
|
||||||
{
|
|
||||||
csv += $"\"{association.SourceName}\",\"{association.SourceType}\",\"{association.SourceKey}\",";
|
|
||||||
csv += $"\"{association.DestinationEntity}\",\"{association.DestinationId}\",\"{association.RestCredentialName}\",";
|
|
||||||
csv += $"\"{(association.IsActive ? "Attiva" : "Disattivata")}\",\"{association.CreatedAt:dd/MM/yyyy HH:mm}\",";
|
|
||||||
csv += $"\"{(association.UpdatedAt?.ToString("dd/MM/yyyy HH:mm") ?? "")}\"\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
var fileName = $"associazioni_record_{DateTime.Now:yyyyMMdd_HHmmss}.csv";
|
|
||||||
var bytes = System.Text.Encoding.UTF8.GetBytes(csv);
|
|
||||||
var base64 = Convert.ToBase64String(bytes);
|
|
||||||
|
|
||||||
await JSRuntime.InvokeVoidAsync("downloadFile", fileName, base64);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogError(ex, "Errore nell'esportazione delle associazioni");
|
|
||||||
await JSRuntime.InvokeVoidAsync("alert", $"Errore nell'esportazione: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ShowClearConfirmation(bool deleteAll)
|
|
||||||
{
|
|
||||||
isDeleteAll = deleteAll;
|
|
||||||
if (deleteAll)
|
|
||||||
{
|
|
||||||
confirmMessage = "Sei sicuro di voler eliminare TUTTE le associazioni dal sistema? Questa operazione non può essere annullata.";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var filteredCount = filteredAssociations.Count;
|
|
||||||
if (filteredCount == 0)
|
|
||||||
{
|
|
||||||
SetOperationMessage("Nessuna associazione da eliminare con i filtri attuali.", "warning");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
confirmMessage = $"Sei sicuro di voler eliminare {filteredCount} associazioni filtrate? Questa operazione non può essere annullata.";
|
|
||||||
}
|
|
||||||
showConfirmModal = true;
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ConfirmClearAssociations()
|
|
||||||
{
|
|
||||||
showConfirmModal = false;
|
|
||||||
isProcessing = true;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
int deletedCount = 0;
|
|
||||||
|
|
||||||
if (isDeleteAll)
|
|
||||||
{
|
|
||||||
processingMessage = "Eliminazione di tutte le associazioni...";
|
|
||||||
StateHasChanged();
|
|
||||||
deletedCount = await CredentialService.ClearAllRecordAssociationsAsync();
|
|
||||||
SetOperationMessage($"Eliminate tutte le {deletedCount} associazioni dal sistema.", "success");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
processingMessage = "Eliminazione associazioni filtrate...";
|
|
||||||
StateHasChanged();
|
|
||||||
|
|
||||||
// Elimina le associazioni filtrate una per una
|
|
||||||
foreach (var association in filteredAssociations.ToList())
|
|
||||||
{
|
|
||||||
await CredentialService.DeleteRecordAssociationAsync(association.Id);
|
|
||||||
deletedCount++;
|
|
||||||
}
|
|
||||||
SetOperationMessage($"Eliminate {deletedCount} associazioni filtrate.", "success");
|
|
||||||
}
|
|
||||||
|
|
||||||
await RefreshAssociations();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogError(ex, "Errore nell'eliminazione delle associazioni");
|
|
||||||
SetOperationMessage($"Errore nell'eliminazione: {ex.Message}", "danger");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
isProcessing = false;
|
|
||||||
processingMessage = "";
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CancelClearConfirmation()
|
|
||||||
{
|
|
||||||
showConfirmModal = false;
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ValidateAllAssociations()
|
|
||||||
{
|
{
|
||||||
isProcessing = true;
|
isProcessing = true;
|
||||||
processingMessage = "Validazione associazioni in corso...";
|
processingMessage = "Validazione associazioni in corso...";
|
||||||
@@ -729,7 +559,7 @@
|
|||||||
|
|
||||||
foreach (var group in uniqueDestinations)
|
foreach (var group in uniqueDestinations)
|
||||||
{
|
{
|
||||||
var invalidAssociations = await CredentialService.GetInvalidRecordAssociationsAsync(
|
var invalidAssociations = await CredentialService.GetInvalidKeyAssociationsAsync(
|
||||||
group.Key.DestinationEntity,
|
group.Key.DestinationEntity,
|
||||||
group.Key.RestCredentialName);
|
group.Key.RestCredentialName);
|
||||||
invalidCount += invalidAssociations.Count;
|
invalidCount += invalidAssociations.Count;
|
||||||
@@ -741,7 +571,7 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
SetOperationMessage($"Trovate {invalidCount} associazioni con ID di destinazione non più validi. Usa 'Pulisci Associazioni Non Valide' per rimuoverle.", "warning");
|
SetOperationMessage($"Trovate {invalidCount} associazioni con ID di destinazione non più validi. Usa 'Pulisci Non Valide' per rimuoverle.", "warning");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -772,7 +602,7 @@
|
|||||||
|
|
||||||
foreach (var group in uniqueDestinations)
|
foreach (var group in uniqueDestinations)
|
||||||
{
|
{
|
||||||
var cleanedCount = await CredentialService.CleanupInvalidRecordAssociationsAsync(
|
var cleanedCount = await CredentialService.CleanupInvalidKeyAssociationsAsync(
|
||||||
group.Key.DestinationEntity,
|
group.Key.DestinationEntity,
|
||||||
group.Key.RestCredentialName);
|
group.Key.RestCredentialName);
|
||||||
totalCleaned += cleanedCount;
|
totalCleaned += cleanedCount;
|
||||||
@@ -784,13 +614,13 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
SetOperationMessage($"Pulite {totalCleaned} associazioni con ID di destinazione non più validi.", "success");
|
SetOperationMessage($"Pulite {totalCleaned} associazioni non valide!", "success");
|
||||||
await RefreshAssociations();
|
await RefreshAssociations();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, "Errore nella pulizia delle associazioni non valide");
|
Logger.LogError(ex, "Errore nella pulizia delle associazioni");
|
||||||
SetOperationMessage($"Errore nella pulizia: {ex.Message}", "danger");
|
SetOperationMessage($"Errore nella pulizia: {ex.Message}", "danger");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -801,50 +631,77 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ExportToCsv()
|
private async Task ExportAssociations()
|
||||||
{
|
{
|
||||||
isProcessing = true;
|
|
||||||
processingMessage = "Esportazione in corso...";
|
|
||||||
StateHasChanged();
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await ExportAssociations();
|
var csv = "Valore Chiave,Campo Sorgente,Campo Destinazione,Entità Destinazione,ID Destinazione,Credenziale,Stato,Creata,Aggiornata,Verificata\n";
|
||||||
SetOperationMessage($"Esportate {filteredAssociations.Count} associazioni in CSV.", "success");
|
|
||||||
|
foreach (var association in filteredAssociations)
|
||||||
|
{
|
||||||
|
csv += $"\"{association.KeyValue}\",\"{association.SourceKeyField}\",\"{association.DestinationKeyField}\",";
|
||||||
|
csv += $"\"{association.DestinationEntity}\",\"{association.DestinationId}\",\"{association.RestCredentialName}\",";
|
||||||
|
csv += $"\"{(association.IsActive ? "Attiva" : "Disattivata")}\",\"{association.CreatedAt:dd/MM/yyyy HH:mm}\",";
|
||||||
|
csv += $"\"{(association.UpdatedAt?.ToString("dd/MM/yyyy HH:mm") ?? "")}\",";
|
||||||
|
csv += $"\"{(association.LastVerifiedAt?.ToString("dd/MM/yyyy HH:mm") ?? "")}\"\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes = System.Text.Encoding.UTF8.GetBytes(csv);
|
||||||
|
var fileName = $"associazioni_chiavi_{DateTime.Now:yyyyMMdd_HHmmss}.csv";
|
||||||
|
|
||||||
|
await JSRuntime.InvokeVoidAsync("downloadFile", fileName, "text/csv", System.Convert.ToBase64String(bytes));
|
||||||
|
SetOperationMessage($"File {fileName} esportato con successo!", "success");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, "Errore nell'esportazione");
|
Logger.LogError(ex, "Errore nell'esportazione");
|
||||||
SetOperationMessage($"Errore nell'esportazione: {ex.Message}", "danger");
|
SetOperationMessage($"Errore nell'esportazione: {ex.Message}", "danger");
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
isProcessing = false;
|
|
||||||
processingMessage = "";
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ShowImportDialog()
|
private async Task ClearAllAssociations()
|
||||||
{
|
{
|
||||||
// Placeholder per import dialog
|
if (await JSRuntime.InvokeAsync<bool>("confirm", "ATTENZIONE: Questa operazione eliminerà TUTTE le associazioni dal sistema. Sei assolutamente sicuro di voler procedere?"))
|
||||||
await JSRuntime.InvokeVoidAsync("alert", "Funzionalità di importazione non ancora implementata.");
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
isProcessing = true;
|
||||||
|
processingMessage = "Eliminazione di tutte le associazioni...";
|
||||||
|
StateHasChanged();
|
||||||
|
|
||||||
|
var deletedCount = await CredentialService.ClearAllKeyAssociationsAsync();
|
||||||
|
|
||||||
|
SetOperationMessage($"Eliminate {deletedCount} associazioni dal sistema!", "success");
|
||||||
|
await RefreshAssociations();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Errore nell'eliminazione di massa");
|
||||||
|
SetOperationMessage($"Errore: {ex.Message}", "danger");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
isProcessing = false;
|
||||||
|
processingMessage = "";
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetOperationMessage(string message, string type)
|
private void SetOperationMessage(string message, string type)
|
||||||
{
|
{
|
||||||
operationMessage = message;
|
operationMessage = message;
|
||||||
operationMessageType = type;
|
operationMessageType = type;
|
||||||
StateHasChanged();
|
|
||||||
|
|
||||||
// Auto-hide success messages after 5 seconds
|
|
||||||
if (type == "success")
|
|
||||||
{
|
|
||||||
_ = Task.Delay(5000).ContinueWith(_ =>
|
|
||||||
{
|
|
||||||
operationMessage = "";
|
|
||||||
StateHasChanged();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.downloadFile = function (fileName, contentType, data) {
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.download = fileName;
|
||||||
|
link.href = 'data:' + contentType + ';base64,' + data;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,316 @@
|
|||||||
|
@page "/test-associations"
|
||||||
|
@using CredentialManager.Models
|
||||||
|
@using DataConnection.CredentialManagement.Interfaces
|
||||||
|
@inject IDataConnectionCredentialService CredentialService
|
||||||
|
@inject ILogger<TestAssociations> Logger
|
||||||
|
|
||||||
|
<PageTitle>Test Associazioni</PageTitle>
|
||||||
|
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h3><i class="fas fa-vial"></i> Test Sistema Associazioni Chiave</h3>
|
||||||
|
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5><i class="fas fa-plus"></i> Crea Associazione Test</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Valore Chiave:</label>
|
||||||
|
<input type="text" class="form-control" @bind="testKeyValue" placeholder="es. 12345" />
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Entità Destinazione:</label>
|
||||||
|
<input type="text" class="form-control" @bind="testDestinationEntity" placeholder="es. BusinessPartners" />
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">ID Destinazione:</label>
|
||||||
|
<input type="text" class="form-control" @bind="testDestinationId" placeholder="es. BP001" />
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Credenziale REST:</label>
|
||||||
|
<input type="text" class="form-control" @bind="testRestCredential" placeholder="es. SAP_B1" />
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" @onclick="CreateTestAssociation">
|
||||||
|
<i class="fas fa-save"></i> Crea Associazione
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5><i class="fas fa-search"></i> Cerca Associazione</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Valore Chiave da Cercare:</label>
|
||||||
|
<input type="text" class="form-control" @bind="searchKeyValue" placeholder="es. 12345" />
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Entità Destinazione:</label>
|
||||||
|
<input type="text" class="form-control" @bind="searchDestinationEntity" placeholder="es. BusinessPartners" />
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Credenziale REST:</label>
|
||||||
|
<input type="text" class="form-control" @bind="searchRestCredential" placeholder="es. SAP_B1" />
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-info" @onclick="SearchAssociation">
|
||||||
|
<i class="fas fa-search"></i> Cerca
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(resultMessage))
|
||||||
|
{
|
||||||
|
<div class="alert @(resultType == "success" ? "alert-success" : resultType == "error" ? "alert-danger" : "alert-info") mt-4">
|
||||||
|
<i class="fas @(resultType == "success" ? "fa-check-circle" : resultType == "error" ? "fa-exclamation-triangle" : "fa-info-circle")"></i>
|
||||||
|
@resultMessage
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (foundAssociation != null)
|
||||||
|
{
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5><i class="fas fa-link"></i> Associazione Trovata</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>ID:</strong> @foundAssociation.Id<br>
|
||||||
|
<strong>Valore Chiave:</strong> @foundAssociation.KeyValue<br>
|
||||||
|
<strong>Campo Chiave Sorgente:</strong> @foundAssociation.SourceKeyField<br>
|
||||||
|
<strong>Campo Chiave Destinazione:</strong> @foundAssociation.DestinationKeyField<br>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Entità Destinazione:</strong> @foundAssociation.DestinationEntity<br>
|
||||||
|
<strong>ID Destinazione:</strong> @foundAssociation.DestinationId<br>
|
||||||
|
<strong>Credenziale REST:</strong> @foundAssociation.RestCredentialName<br>
|
||||||
|
<strong>Attiva:</strong> @(foundAssociation.IsActive ? "Sì" : "No")<br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-2">
|
||||||
|
<div class="col-12">
|
||||||
|
<strong>Creata:</strong> @foundAssociation.CreatedAt.ToString("dd/MM/yyyy HH:mm:ss")<br>
|
||||||
|
@if (foundAssociation.UpdatedAt.HasValue)
|
||||||
|
{
|
||||||
|
<strong>Aggiornata:</strong> @foundAssociation.UpdatedAt.Value.ToString("dd/MM/yyyy HH:mm:ss")<br>
|
||||||
|
}
|
||||||
|
@if (foundAssociation.LastVerifiedAt.HasValue)
|
||||||
|
{
|
||||||
|
<strong>Ultima Verifica:</strong> @foundAssociation.LastVerifiedAt.Value.ToString("dd/MM/yyyy HH:mm:ss")<br>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5><i class="fas fa-list"></i> Tutte le Associazioni Attive</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<button class="btn btn-secondary mb-3" @onclick="LoadAllAssociations">
|
||||||
|
<i class="fas fa-refresh"></i> Carica Associazioni
|
||||||
|
</button>
|
||||||
|
|
||||||
|
@if (allAssociations != null && allAssociations.Any())
|
||||||
|
{
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Valore Chiave</th>
|
||||||
|
<th>Entità</th>
|
||||||
|
<th>ID Destinazione</th>
|
||||||
|
<th>Credenziale</th>
|
||||||
|
<th>Creata</th>
|
||||||
|
<th>Azioni</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var assoc in allAssociations)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>@assoc.Id</td>
|
||||||
|
<td>@assoc.KeyValue</td>
|
||||||
|
<td>@assoc.DestinationEntity</td>
|
||||||
|
<td>@assoc.DestinationId</td>
|
||||||
|
<td>@assoc.RestCredentialName</td>
|
||||||
|
<td>@assoc.CreatedAt.ToString("dd/MM/yyyy HH:mm")</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-danger" @onclick="() => DeleteAssociation(assoc.Id)">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (allAssociations != null)
|
||||||
|
{
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="fas fa-info-circle"></i> Nessuna associazione trovata.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private string testKeyValue = "";
|
||||||
|
private string testDestinationEntity = "";
|
||||||
|
private string testDestinationId = "";
|
||||||
|
private string testRestCredential = "";
|
||||||
|
|
||||||
|
private string searchKeyValue = "";
|
||||||
|
private string searchDestinationEntity = "";
|
||||||
|
private string searchRestCredential = "";
|
||||||
|
|
||||||
|
private string resultMessage = "";
|
||||||
|
private string resultType = "";
|
||||||
|
private KeyAssociation? foundAssociation;
|
||||||
|
private List<KeyAssociation>? allAssociations;
|
||||||
|
|
||||||
|
private async Task CreateTestAssociation()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(testKeyValue) || string.IsNullOrEmpty(testDestinationEntity) ||
|
||||||
|
string.IsNullOrEmpty(testDestinationId) || string.IsNullOrEmpty(testRestCredential))
|
||||||
|
{
|
||||||
|
resultMessage = "Tutti i campi sono obbligatori.";
|
||||||
|
resultType = "error";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var association = new KeyAssociation
|
||||||
|
{
|
||||||
|
KeyValue = testKeyValue,
|
||||||
|
SourceKeyField = "test_field",
|
||||||
|
DestinationKeyField = "id",
|
||||||
|
DestinationEntity = testDestinationEntity,
|
||||||
|
DestinationId = testDestinationId,
|
||||||
|
RestCredentialName = testRestCredential,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
LastVerifiedAt = DateTime.UtcNow,
|
||||||
|
AdditionalInfo = "{\"test\": true}"
|
||||||
|
};
|
||||||
|
|
||||||
|
var id = await CredentialService.SaveKeyAssociationAsync(association);
|
||||||
|
resultMessage = $"Associazione creata con successo! ID: {id}";
|
||||||
|
resultType = "success";
|
||||||
|
|
||||||
|
Logger.LogInformation("Associazione test creata: ID={Id}, KeyValue={KeyValue}", id, testKeyValue);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
resultMessage = $"Errore nella creazione: {ex.Message}";
|
||||||
|
resultType = "error";
|
||||||
|
Logger.LogError(ex, "Errore nella creazione dell'associazione test");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SearchAssociation()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(searchKeyValue))
|
||||||
|
{
|
||||||
|
resultMessage = "Valore chiave obbligatorio per la ricerca.";
|
||||||
|
resultType = "error";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foundAssociation = null;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(searchDestinationEntity) && !string.IsNullOrEmpty(searchRestCredential))
|
||||||
|
{
|
||||||
|
foundAssociation = await CredentialService.FindKeyAssociationByValueAsync(
|
||||||
|
searchKeyValue, searchDestinationEntity, searchRestCredential);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foundAssociation = await CredentialService.FindKeyAssociationByValueAsync(searchKeyValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundAssociation != null)
|
||||||
|
{
|
||||||
|
resultMessage = "Associazione trovata!";
|
||||||
|
resultType = "success";
|
||||||
|
Logger.LogInformation("Associazione trovata: ID={Id} per KeyValue={KeyValue}", foundAssociation.Id, searchKeyValue);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
resultMessage = "Nessuna associazione trovata con i criteri specificati.";
|
||||||
|
resultType = "info";
|
||||||
|
Logger.LogInformation("Nessuna associazione trovata per KeyValue={KeyValue}", searchKeyValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
resultMessage = $"Errore nella ricerca: {ex.Message}";
|
||||||
|
resultType = "error";
|
||||||
|
Logger.LogError(ex, "Errore nella ricerca dell'associazione");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadAllAssociations()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
allAssociations = await CredentialService.GetAllActiveKeyAssociationsAsync();
|
||||||
|
resultMessage = $"Caricate {allAssociations.Count} associazioni attive.";
|
||||||
|
resultType = "info";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
resultMessage = $"Errore nel caricamento: {ex.Message}";
|
||||||
|
resultType = "error";
|
||||||
|
Logger.LogError(ex, "Errore nel caricamento delle associazioni");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DeleteAssociation(int id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await CredentialService.DeleteKeyAssociationAsync(id);
|
||||||
|
if (result)
|
||||||
|
{
|
||||||
|
resultMessage = $"Associazione {id} eliminata con successo.";
|
||||||
|
resultType = "success";
|
||||||
|
await LoadAllAssociations(); // Ricarica la lista
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
resultMessage = $"Errore nell'eliminazione dell'associazione {id}.";
|
||||||
|
resultType = "error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
resultMessage = $"Errore nell'eliminazione: {ex.Message}";
|
||||||
|
resultType = "error";
|
||||||
|
Logger.LogError(ex, "Errore nell'eliminazione dell'associazione {Id}", id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await LoadAllAssociations();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,11 +18,13 @@
|
|||||||
<NavLink class="nav-link" href="counter">
|
<NavLink class="nav-link" href="counter">
|
||||||
<span class="oi oi-plus" aria-hidden="true"></span> Counter
|
<span class="oi oi-plus" aria-hidden="true"></span> Counter
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div> <div class="nav-item px-3">
|
</div>
|
||||||
|
<div class="nav-item px-3">
|
||||||
<NavLink class="nav-link" href="fetchdata">
|
<NavLink class="nav-link" href="fetchdata">
|
||||||
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
|
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div> <div class="nav-item px-3">
|
</div>
|
||||||
|
<div class="nav-item px-3">
|
||||||
<NavLink class="nav-link" href="credentials">
|
<NavLink class="nav-link" href="credentials">
|
||||||
<span class="oi oi-key" aria-hidden="true"></span> Gestione Credenziali
|
<span class="oi oi-key" aria-hidden="true"></span> Gestione Credenziali
|
||||||
</NavLink>
|
</NavLink>
|
||||||
@@ -33,8 +35,8 @@
|
|||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item px-3">
|
<div class="nav-item px-3">
|
||||||
<NavLink class="nav-link" href="record-associations">
|
<NavLink class="nav-link" href="key-associations">
|
||||||
<span class="oi oi-link-intact" aria-hidden="true"></span> Associazioni Record
|
<span class="oi oi-link-intact" aria-hidden="true"></span> Gestione Associazioni Chiave
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user