diff --git a/CredentialManager/Data/CredentialDbContext.cs b/CredentialManager/Data/CredentialDbContext.cs index df51b73..5069d45 100644 --- a/CredentialManager/Data/CredentialDbContext.cs +++ b/CredentialManager/Data/CredentialDbContext.cs @@ -9,7 +9,7 @@ namespace CredentialManager.Data; public class CredentialDbContext : DbContext { public DbSet Credentials { get; set; } - public DbSet RecordAssociations { get; set; } + public DbSet KeyAssociations { get; set; } public CredentialDbContext(DbContextOptions options) : base(options) { @@ -86,24 +86,24 @@ public class CredentialDbContext : DbContext entity.HasIndex(e => e.IsActive); }); - // Configurazione della tabella RecordAssociations - modelBuilder.Entity(entity => + // Configurazione della tabella KeyAssociations + modelBuilder.Entity(entity => { - entity.ToTable("RecordAssociations"); + entity.ToTable("KeyAssociations"); entity.HasKey(e => e.Id); - entity.Property(e => e.SourceName) + entity.Property(e => e.KeyValue) + .IsRequired() + .HasMaxLength(500); + + entity.Property(e => e.SourceKeyField) .IsRequired() .HasMaxLength(200); - entity.Property(e => e.SourceType) + entity.Property(e => e.DestinationKeyField) .IsRequired() - .HasMaxLength(50); - - entity.Property(e => e.SourceKey) - .IsRequired() - .HasMaxLength(500); + .HasMaxLength(200); entity.Property(e => e.DestinationEntity) .IsRequired() @@ -117,6 +117,9 @@ public class CredentialDbContext : DbContext .IsRequired() .HasMaxLength(100); + entity.Property(e => e.SourcesInfo) + .HasMaxLength(2000); + entity.Property(e => e.AdditionalInfo) .HasMaxLength(2000); @@ -125,15 +128,18 @@ public class CredentialDbContext : DbContext .HasDefaultValue(true); // Indici - entity.HasIndex(e => new { e.SourceName, e.SourceKey, e.DestinationEntity }) - .IsUnique() - .HasDatabaseName("IX_RecordAssociations_Unique"); + entity.HasIndex(e => e.KeyValue) + .HasDatabaseName("IX_KeyAssociations_KeyValue"); + + 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.RestCredentialName); entity.HasIndex(e => e.IsActive); entity.HasIndex(e => e.CreatedAt); + entity.HasIndex(e => e.LastVerifiedAt); }); } } diff --git a/CredentialManager/Migrations/20250617_AddRestServiceType.cs b/CredentialManager/Migrations/20250617_AddRestServiceType.cs deleted file mode 100644 index a792636..0000000 --- a/CredentialManager/Migrations/20250617_AddRestServiceType.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace CredentialManager.Migrations -{ - /// - public partial class AddRestServiceType : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "RestServiceType", - table: "Credentials", - type: "TEXT", - maxLength: 50, - nullable: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "RestServiceType", - table: "Credentials"); - } - } -} diff --git a/CredentialManager/Migrations/20250628_AddRecordAssociations.cs b/CredentialManager/Migrations/20250628_AddRecordAssociations.cs deleted file mode 100644 index eedf7dc..0000000 --- a/CredentialManager/Migrations/20250628_AddRecordAssociations.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace CredentialManager.Migrations -{ - /// - /// Aggiunge la tabella RecordAssociations per tracciare le associazioni tra record sorgente e destinazione - /// - public partial class AddRecordAssociations : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "RecordAssociations", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - SourceName = table.Column(type: "TEXT", maxLength: 200, nullable: false), - SourceType = table.Column(type: "TEXT", maxLength: 50, nullable: false), - SourceKey = table.Column(type: "TEXT", maxLength: 500, nullable: false), - DestinationEntity = table.Column(type: "TEXT", maxLength: 200, nullable: false), - DestinationId = table.Column(type: "TEXT", maxLength: 200, nullable: false), - RestCredentialName = table.Column(type: "TEXT", maxLength: 100, nullable: false), - CreatedAt = table.Column(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')"), - UpdatedAt = table.Column(type: "TEXT", nullable: true), - IsActive = table.Column(type: "INTEGER", nullable: false, defaultValue: true), - AdditionalInfo = table.Column(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"); - } - } -} diff --git a/CredentialManager/Migrations/20250629181214_ReplaceRecordAssociationsWithKeyAssociations.Designer.cs b/CredentialManager/Migrations/20250629181214_ReplaceRecordAssociationsWithKeyAssociations.Designer.cs new file mode 100644 index 0000000..176caba --- /dev/null +++ b/CredentialManager/Migrations/20250629181214_ReplaceRecordAssociationsWithKeyAssociations.Designer.cs @@ -0,0 +1,211 @@ +// +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 + { + /// + 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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdditionalParameters") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("CommandTimeout") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(30); + + b.Property("ConnectionString") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DatabaseName") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DatabaseType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("EncryptedApiKey") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("EncryptedAuthToken") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("EncryptedPassword") + .HasColumnType("TEXT"); + + b.Property("Headers") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("Host") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("IgnoreSslErrors") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.Property("RestServiceType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TimeoutSeconds") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(100); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdditionalInfo") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DestinationEntity") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DestinationId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DestinationKeyField") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("KeyValue") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("LastVerifiedAt") + .HasColumnType("TEXT"); + + b.Property("RestCredentialName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SourceKeyField") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SourcesInfo") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("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 + } + } +} diff --git a/CredentialManager/Migrations/20250629181214_ReplaceRecordAssociationsWithKeyAssociations.cs b/CredentialManager/Migrations/20250629181214_ReplaceRecordAssociationsWithKeyAssociations.cs new file mode 100644 index 0000000..91f1d11 --- /dev/null +++ b/CredentialManager/Migrations/20250629181214_ReplaceRecordAssociationsWithKeyAssociations.cs @@ -0,0 +1,139 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CredentialManager.Migrations +{ + /// + public partial class ReplaceRecordAssociationsWithKeyAssociations : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Credentials", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(type: "TEXT", maxLength: 100, nullable: false), + Type = table.Column(type: "TEXT", maxLength: 50, nullable: false), + DatabaseType = table.Column(type: "TEXT", maxLength: 50, nullable: true), + ConnectionString = table.Column(type: "TEXT", maxLength: 500, nullable: true), + Host = table.Column(type: "TEXT", maxLength: 200, nullable: true), + Port = table.Column(type: "INTEGER", nullable: true), + DatabaseName = table.Column(type: "TEXT", maxLength: 100, nullable: true), + Username = table.Column(type: "TEXT", maxLength: 100, nullable: true), + EncryptedPassword = table.Column(type: "TEXT", nullable: true), + EncryptedApiKey = table.Column(type: "TEXT", maxLength: 500, nullable: true), + EncryptedAuthToken = table.Column(type: "TEXT", maxLength: 500, nullable: true), + CommandTimeout = table.Column(type: "INTEGER", nullable: false, defaultValue: 30), + TimeoutSeconds = table.Column(type: "INTEGER", nullable: false, defaultValue: 100), + IgnoreSslErrors = table.Column(type: "INTEGER", nullable: false, defaultValue: false), + RestServiceType = table.Column(type: "TEXT", maxLength: 50, nullable: true), + Headers = table.Column(type: "TEXT", maxLength: 2000, nullable: true), + AdditionalParameters = table.Column(type: "TEXT", maxLength: 2000, nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IsActive = table.Column(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(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + KeyValue = table.Column(type: "TEXT", maxLength: 500, nullable: false), + SourceKeyField = table.Column(type: "TEXT", maxLength: 200, nullable: false), + DestinationKeyField = table.Column(type: "TEXT", maxLength: 200, nullable: false), + DestinationEntity = table.Column(type: "TEXT", maxLength: 200, nullable: false), + DestinationId = table.Column(type: "TEXT", maxLength: 200, nullable: false), + RestCredentialName = table.Column(type: "TEXT", maxLength: 100, nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + LastVerifiedAt = table.Column(type: "TEXT", nullable: true), + IsActive = table.Column(type: "INTEGER", nullable: false, defaultValue: true), + SourcesInfo = table.Column(type: "TEXT", maxLength: 2000, nullable: true), + AdditionalInfo = table.Column(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); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Credentials"); + + migrationBuilder.DropTable( + name: "KeyAssociations"); + } + } +} diff --git a/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs b/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs new file mode 100644 index 0000000..925feee --- /dev/null +++ b/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs @@ -0,0 +1,208 @@ +// +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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdditionalParameters") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("CommandTimeout") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(30); + + b.Property("ConnectionString") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DatabaseName") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DatabaseType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("EncryptedApiKey") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("EncryptedAuthToken") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("EncryptedPassword") + .HasColumnType("TEXT"); + + b.Property("Headers") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("Host") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("IgnoreSslErrors") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.Property("RestServiceType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TimeoutSeconds") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(100); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdditionalInfo") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DestinationEntity") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DestinationId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DestinationKeyField") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("KeyValue") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("LastVerifiedAt") + .HasColumnType("TEXT"); + + b.Property("RestCredentialName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SourceKeyField") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SourcesInfo") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("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 + } + } +} diff --git a/CredentialManager/Models/RecordAssociation.cs b/CredentialManager/Models/KeyAssociation.cs similarity index 54% rename from CredentialManager/Models/RecordAssociation.cs rename to CredentialManager/Models/KeyAssociation.cs index 3d9b4fa..f3591f8 100644 --- a/CredentialManager/Models/RecordAssociation.cs +++ b/CredentialManager/Models/KeyAssociation.cs @@ -3,33 +3,38 @@ using System.ComponentModel.DataAnnotations; namespace CredentialManager.Models; /// -/// 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 /// -public class RecordAssociation +public class KeyAssociation { [Key] public int Id { get; set; } /// - /// Nome della sorgente dati (nome tabella/file/foglio) - /// - [Required] - [MaxLength(200)] - public string SourceName { get; set; } = string.Empty; - - /// - /// Tipo di sorgente (database, file) - /// - [Required] - [MaxLength(50)] - public string SourceType { get; set; } = string.Empty; - - /// - /// Chiave del record sorgente (può essere un ID o una combinazione di campi) + /// Valore della chiave che identifica univocamente l'oggetto business + /// (es: "CUST001", "12345", "ABC-DEF-GHI") /// [Required] [MaxLength(500)] - public string SourceKey { get; set; } = string.Empty; + public string KeyValue { get; set; } = string.Empty; + + /// + /// Nome del campo chiave nella sorgente + /// (es: "CustomerCode", "ID", "ArticleNumber") + /// + [Required] + [MaxLength(200)] + public string SourceKeyField { get; set; } = string.Empty; + + /// + /// Nome del campo chiave nella destinazione + /// (es: "CardCode", "DocEntry", "ItemCode") + /// + [Required] + [MaxLength(200)] + public string DestinationKeyField { get; set; } = string.Empty; /// /// Nome dell'entità di destinazione @@ -46,7 +51,7 @@ public class RecordAssociation public string DestinationId { get; set; } = string.Empty; /// - /// Nome della credenziale REST utilizzata + /// Nome della credenziale REST utilizzata per la destinazione /// [Required] [MaxLength(100)] @@ -62,11 +67,22 @@ public class RecordAssociation /// public DateTime? UpdatedAt { get; set; } + /// + /// Data e ora dell'ultima verifica che il record di destinazione esiste ancora + /// + public DateTime? LastVerifiedAt { get; set; } + /// /// Indica se l'associazione è ancora attiva /// public bool IsActive { get; set; } = true; + /// + /// Informazioni aggiuntive sui record che hanno contribuito a questa associazione + /// + [MaxLength(2000)] + public string? SourcesInfo { get; set; } + /// /// Informazioni aggiuntive in formato JSON /// diff --git a/CredentialManager/Services/DatabaseInitializer.cs b/CredentialManager/Services/DatabaseInitializer.cs index a656cb5..9893749 100644 --- a/CredentialManager/Services/DatabaseInitializer.cs +++ b/CredentialManager/Services/DatabaseInitializer.cs @@ -68,16 +68,16 @@ public class DatabaseInitializer : IDatabaseInitializer await _context.Credentials.CountAsync(); _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 { - await _context.RecordAssociations.CountAsync(); - _logger.LogInformation("Tabella RecordAssociations verificata con successo"); + await _context.KeyAssociations.CountAsync(); + _logger.LogInformation("Tabella KeyAssociations verificata con successo"); } catch (Exception) { - _logger.LogInformation("Tabella RecordAssociations non trovata, creazione tramite migrazione..."); - await CreateRecordAssociationsTableAsync(); + _logger.LogInformation("Tabella KeyAssociations non trovata, creazione tramite migrazione..."); + await CreateKeyAssociationsTableAsync(); } } catch (Exception) @@ -170,60 +170,78 @@ public class DatabaseInitializer : IDatabaseInitializer _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 { await _context.Database.ExecuteSqlRawAsync( - "SELECT COUNT(*) FROM RecordAssociations LIMIT 1"); - _logger.LogInformation("Tabella RecordAssociations già presente"); + "SELECT COUNT(*) FROM KeyAssociations LIMIT 1"); + _logger.LogInformation("Tabella KeyAssociations già presente"); } catch (Microsoft.Data.Sqlite.SqliteException) { // La tabella non esiste, la creiamo - _logger.LogInformation("Creazione tabella RecordAssociations..."); + _logger.LogInformation("Creazione tabella KeyAssociations..."); // Crea la tabella await _context.Database.ExecuteSqlRawAsync(@" - CREATE TABLE RecordAssociations ( + CREATE TABLE KeyAssociations ( Id INTEGER PRIMARY KEY AUTOINCREMENT, - SourceName TEXT NOT NULL, - SourceType TEXT NOT NULL, - SourceKey TEXT NOT NULL, + KeyValue TEXT NOT NULL, + SourceKeyField TEXT NOT NULL, + DestinationKeyField TEXT NOT NULL, DestinationEntity TEXT NOT NULL, DestinationId TEXT NOT NULL, RestCredentialName TEXT NOT NULL, CreatedAt TEXT NOT NULL DEFAULT (datetime('now')), UpdatedAt TEXT, + LastVerifiedAt TEXT, IsActive INTEGER NOT NULL DEFAULT 1, + SourcesInfo TEXT, AdditionalInfo TEXT )"); // Crea gli indici await _context.Database.ExecuteSqlRawAsync(@" - CREATE UNIQUE INDEX IX_RecordAssociations_Unique - ON RecordAssociations (SourceName, SourceKey, DestinationEntity)"); + CREATE INDEX IX_KeyAssociations_KeyValue + ON KeyAssociations (KeyValue)"); await _context.Database.ExecuteSqlRawAsync(@" - CREATE INDEX IX_RecordAssociations_SourceType - ON RecordAssociations (SourceType)"); + CREATE UNIQUE INDEX IX_KeyAssociations_Unique + ON KeyAssociations (KeyValue, DestinationEntity, RestCredentialName)"); await _context.Database.ExecuteSqlRawAsync(@" - CREATE INDEX IX_RecordAssociations_DestinationEntity - ON RecordAssociations (DestinationEntity)"); + CREATE INDEX IX_KeyAssociations_DestinationEntity + ON KeyAssociations (DestinationEntity)"); await _context.Database.ExecuteSqlRawAsync(@" - CREATE INDEX IX_RecordAssociations_RestCredentialName - ON RecordAssociations (RestCredentialName)"); + CREATE INDEX IX_KeyAssociations_RestCredentialName + ON KeyAssociations (RestCredentialName)"); await _context.Database.ExecuteSqlRawAsync(@" - CREATE INDEX IX_RecordAssociations_IsActive - ON RecordAssociations (IsActive)"); + CREATE INDEX IX_KeyAssociations_IsActive + ON KeyAssociations (IsActive)"); await _context.Database.ExecuteSqlRawAsync(@" - CREATE INDEX IX_RecordAssociations_CreatedAt - ON RecordAssociations (CreatedAt)"); + CREATE INDEX IX_KeyAssociations_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) @@ -233,58 +251,67 @@ public class DatabaseInitializer : IDatabaseInitializer } } - private async Task CreateRecordAssociationsTableAsync() + private async Task CreateKeyAssociationsTableAsync() { 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(@" - CREATE TABLE RecordAssociations ( + CREATE TABLE KeyAssociations ( Id INTEGER PRIMARY KEY AUTOINCREMENT, - SourceName TEXT NOT NULL, - SourceType TEXT NOT NULL, - SourceKey TEXT NOT NULL, + KeyValue TEXT NOT NULL, + SourceKeyField TEXT NOT NULL, + DestinationKeyField TEXT NOT NULL, DestinationEntity TEXT NOT NULL, DestinationId TEXT NOT NULL, RestCredentialName TEXT NOT NULL, CreatedAt TEXT NOT NULL DEFAULT (datetime('now')), UpdatedAt TEXT, + LastVerifiedAt TEXT, IsActive INTEGER NOT NULL DEFAULT 1, + SourcesInfo TEXT, AdditionalInfo TEXT )"); // Crea gli indici await _context.Database.ExecuteSqlRawAsync(@" - CREATE UNIQUE INDEX IX_RecordAssociations_Unique - ON RecordAssociations (SourceName, SourceKey, DestinationEntity)"); + CREATE INDEX IX_KeyAssociations_KeyValue + ON KeyAssociations (KeyValue)"); await _context.Database.ExecuteSqlRawAsync(@" - CREATE INDEX IX_RecordAssociations_SourceType - ON RecordAssociations (SourceType)"); + CREATE UNIQUE INDEX IX_KeyAssociations_Unique + ON KeyAssociations (KeyValue, DestinationEntity, RestCredentialName)"); await _context.Database.ExecuteSqlRawAsync(@" - CREATE INDEX IX_RecordAssociations_DestinationEntity - ON RecordAssociations (DestinationEntity)"); + CREATE INDEX IX_KeyAssociations_DestinationEntity + ON KeyAssociations (DestinationEntity)"); await _context.Database.ExecuteSqlRawAsync(@" - CREATE INDEX IX_RecordAssociations_RestCredentialName - ON RecordAssociations (RestCredentialName)"); + CREATE INDEX IX_KeyAssociations_RestCredentialName + ON KeyAssociations (RestCredentialName)"); await _context.Database.ExecuteSqlRawAsync(@" - CREATE INDEX IX_RecordAssociations_IsActive - ON RecordAssociations (IsActive)"); + CREATE INDEX IX_KeyAssociations_IsActive + ON KeyAssociations (IsActive)"); await _context.Database.ExecuteSqlRawAsync(@" - CREATE INDEX IX_RecordAssociations_CreatedAt - ON RecordAssociations (CreatedAt)"); + CREATE INDEX IX_KeyAssociations_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) { - _logger.LogError(ex, "Errore nella creazione della tabella RecordAssociations"); + _logger.LogError(ex, "Errore nella creazione della tabella KeyAssociations"); throw; } } diff --git a/CredentialManager/Services/IKeyAssociationService.cs b/CredentialManager/Services/IKeyAssociationService.cs new file mode 100644 index 0000000..7d3a525 --- /dev/null +++ b/CredentialManager/Services/IKeyAssociationService.cs @@ -0,0 +1,109 @@ +using CredentialManager.Models; + +namespace CredentialManager.Services; + +/// +/// Interfaccia per il servizio di gestione delle associazioni basate sui valori delle chiavi +/// +public interface IKeyAssociationService +{ + /// + /// Salva una nuova associazione o aggiorna una esistente + /// + Task SaveAssociationAsync(KeyAssociation association); + + /// + /// Cerca un'associazione esistente tramite valore chiave + /// + Task FindAssociationByKeyValueAsync(string keyValue, string destinationEntity, string restCredentialName); + + /// + /// Cerca un'associazione esistente tramite valore chiave, indipendentemente dalla destinazione + /// + Task FindAssociationByKeyValueAsync(string keyValue); + + /// + /// Ottiene tutte le associazioni per un'entità di destinazione specifica + /// + Task> GetAssociationsByDestinationAsync(string destinationEntity, string restCredentialName); + + /// + /// Ottiene tutte le associazioni attive + /// + Task> GetAllActiveAssociationsAsync(); + + /// + /// Ottiene tutte le associazioni (attive e non) + /// + Task> GetAllAssociationsAsync(); + + /// + /// Aggiorna un'associazione esistente + /// + Task UpdateAssociationAsync(KeyAssociation association); + + /// + /// Disattiva un'associazione + /// + Task DeactivateAssociationAsync(int id); + + /// + /// Elimina definitivamente un'associazione + /// + Task DeleteAssociationAsync(int id); + + /// + /// Pulisce le associazioni più vecchie di un determinato periodo + /// + Task CleanupOldAssociationsAsync(TimeSpan olderThan); + + /// + /// Elimina tutte le associazioni per una specifica combinazione entità-credenziale + /// + Task ClearAssociationsAsync(string destinationEntity, string restCredentialName); + + /// + /// Elimina tutte le associazioni nel sistema + /// + Task ClearAllAssociationsAsync(); + + /// + /// Verifica se un ID di destinazione esiste ancora nel sistema target + /// + Task ValidateDestinationIdAsync(string destinationId, string destinationEntity, string restCredentialName); + + /// + /// Ottiene tutte le associazioni con ID di destinazione non validi + /// + Task> GetInvalidAssociationsAsync(string destinationEntity, string restCredentialName); + + /// + /// Pulisce le associazioni con ID di destinazione non più validi + /// + Task CleanupInvalidAssociationsAsync(string destinationEntity, string restCredentialName); + + /// + /// Aggiorna la data di ultima verifica per un'associazione + /// + Task UpdateLastVerifiedAsync(int id); + + /// + /// Ottiene statistiche sulle associazioni + /// + Task GetStatisticsAsync(); +} + +/// +/// Statistiche sulle associazioni +/// +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 AssociationsByEntity { get; set; } = new(); +} diff --git a/CredentialManager/Services/IRecordAssociationService.cs b/CredentialManager/Services/IRecordAssociationService.cs deleted file mode 100644 index 12bef3c..0000000 --- a/CredentialManager/Services/IRecordAssociationService.cs +++ /dev/null @@ -1,79 +0,0 @@ -using CredentialManager.Models; - -namespace CredentialManager.Services; - -/// -/// Interfaccia per il servizio di gestione delle associazioni record -/// -public interface IRecordAssociationService -{ - /// - /// Salva una nuova associazione tra record sorgente e destinazione - /// - Task SaveAssociationAsync(RecordAssociation association); - - /// - /// Cerca un'associazione esistente tramite chiave sorgente - /// - Task FindAssociationAsync(string sourceName, string sourceKey, string destinationEntity); - - /// - /// Ottiene tutte le associazioni per una sorgente specifica - /// - Task> GetAssociationsBySourceAsync(string sourceName, string sourceType); - - /// - /// Ottiene tutte le associazioni per un'entità di destinazione specifica - /// - Task> GetAssociationsByDestinationAsync(string destinationEntity, string restCredentialName); - - /// - /// Ottiene tutte le associazioni attive - /// - Task> GetAllActiveAssociationsAsync(); - - /// - /// Aggiorna un'associazione esistente - /// - Task UpdateAssociationAsync(RecordAssociation association); - - /// - /// Disattiva un'associazione (soft delete) - /// - Task DeactivateAssociationAsync(int id); - - /// - /// Elimina definitivamente un'associazione - /// - Task DeleteAssociationAsync(int id); - - /// - /// Pulisce le associazioni obsolete (opzionale) - /// - Task CleanupOldAssociationsAsync(TimeSpan olderThan); - - /// - /// Elimina tutte le associazioni per una specifica combinazione sorgente-destinazione - /// - Task ClearAssociationsAsync(string sourceName, string destinationEntity, string restCredentialName); - - /// - /// Elimina tutte le associazioni nel sistema - /// - Task ClearAllAssociationsAsync(); - - /// - /// Verifica se un ID di destinazione esiste ancora nel sistema target - /// - Task ValidateDestinationIdAsync(string destinationId, string destinationEntity, string restCredentialName); - - /// - /// Ottiene tutte le associazioni con ID di destinazione non validi - /// - Task> GetInvalidAssociationsAsync(string destinationEntity, string restCredentialName); - - /// - /// Pulisce le associazioni con ID di destinazione non più validi - /// - Task CleanupInvalidAssociationsAsync(string destinationEntity, string restCredentialName); -} diff --git a/CredentialManager/Services/KeyAssociationService.cs b/CredentialManager/Services/KeyAssociationService.cs new file mode 100644 index 0000000..9ac3386 --- /dev/null +++ b/CredentialManager/Services/KeyAssociationService.cs @@ -0,0 +1,494 @@ +using CredentialManager.Data; +using CredentialManager.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace CredentialManager.Services; + +/// +/// Servizio per la gestione delle associazioni basate sui valori delle chiavi +/// +public class KeyAssociationService : IKeyAssociationService +{ + private readonly CredentialDbContext _context; + private readonly ILogger _logger; + + public KeyAssociationService( + CredentialDbContext context, + ILogger logger) + { + _context = context; + _logger = logger; + } + + public async Task 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 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 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> 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> 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> 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 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 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 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 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 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 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 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> 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(); + + // 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 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 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 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"); + } + } +} diff --git a/CredentialManager/Services/RecordAssociationService.cs b/CredentialManager/Services/RecordAssociationService.cs deleted file mode 100644 index 60a38fc..0000000 --- a/CredentialManager/Services/RecordAssociationService.cs +++ /dev/null @@ -1,381 +0,0 @@ -using CredentialManager.Data; -using CredentialManager.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace CredentialManager.Services; - -/// -/// Servizio per la gestione delle associazioni tra record sorgente e destinazione -/// -public class RecordAssociationService : IRecordAssociationService -{ - private readonly CredentialDbContext _context; - private readonly ILogger _logger; - - public RecordAssociationService( - CredentialDbContext context, - ILogger logger) - { - _context = context; - _logger = logger; - } - - public async Task 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 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> 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> 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> 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 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 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 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 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 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 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 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> 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(); - - // 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 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; - } - } -} diff --git a/DataConnection/CredentialManagement/Interfaces/IDataConnectionCredentialService.cs b/DataConnection/CredentialManagement/Interfaces/IDataConnectionCredentialService.cs index ac7127e..92f0fad 100644 --- a/DataConnection/CredentialManagement/Interfaces/IDataConnectionCredentialService.cs +++ b/DataConnection/CredentialManagement/Interfaces/IDataConnectionCredentialService.cs @@ -1,4 +1,5 @@ using CredentialManager.Models; +using CredentialManager.Services; 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(SalesforceCredential credential); - // Record associations - Task SaveRecordAssociationAsync(RecordAssociation association); - Task FindRecordAssociationAsync(string sourceName, string sourceKey, string destinationEntity); - Task> GetRecordAssociationsBySourceAsync(string sourceName, string sourceType); - Task> GetRecordAssociationsByDestinationAsync(string destinationEntity, string restCredentialName); - Task> GetAllActiveRecordAssociationsAsync(); - Task UpdateRecordAssociationAsync(RecordAssociation association); - Task DeactivateRecordAssociationAsync(int id); - Task DeleteRecordAssociationAsync(int id); - Task ClearRecordAssociationsAsync(string sourceName, string destinationEntity, string restCredentialName); - Task ClearAllRecordAssociationsAsync(); - Task> GetInvalidRecordAssociationsAsync(string destinationEntity, string restCredentialName); - Task CleanupInvalidRecordAssociationsAsync(string destinationEntity, string restCredentialName); + // Key associations + Task SaveKeyAssociationAsync(KeyAssociation association); + Task FindKeyAssociationByValueAsync(string keyValue, string destinationEntity, string restCredentialName); + Task FindKeyAssociationByValueAsync(string keyValue); + Task> GetKeyAssociationsByDestinationAsync(string destinationEntity, string restCredentialName); + Task> GetAllActiveKeyAssociationsAsync(); + Task> GetAllKeyAssociationsAsync(); + Task UpdateKeyAssociationAsync(KeyAssociation association); + Task DeactivateKeyAssociationAsync(int id); + Task DeleteKeyAssociationAsync(int id); + Task ClearKeyAssociationsAsync(string destinationEntity, string restCredentialName); + Task ClearAllKeyAssociationsAsync(); + Task> GetInvalidKeyAssociationsAsync(string destinationEntity, string restCredentialName); + Task CleanupInvalidKeyAssociationsAsync(string destinationEntity, string restCredentialName); + Task UpdateKeyAssociationLastVerifiedAsync(int id); + Task GetKeyAssociationStatisticsAsync(); } diff --git a/DataConnection/CredentialManagement/ServiceCollectionExtensions.cs b/DataConnection/CredentialManagement/ServiceCollectionExtensions.cs index 1d1e6a9..fc339d2 100644 --- a/DataConnection/CredentialManagement/ServiceCollectionExtensions.cs +++ b/DataConnection/CredentialManagement/ServiceCollectionExtensions.cs @@ -38,8 +38,8 @@ public static class ServiceCollectionExtensions // Aggiungi i servizi base di CredentialManager services.AddCredentialManager(databasePath); - // Aggiungi il servizio di gestione associazioni record - services.AddScoped(); + // Aggiungi il servizio di gestione associazioni per chiavi + services.AddScoped(); // Aggiungi il servizio di integrazione DataConnection services.AddScoped(); diff --git a/DataConnection/CredentialManagement/Services/DataConnectionCredentialService.cs b/DataConnection/CredentialManagement/Services/DataConnectionCredentialService.cs index 37bf7cb..03eafcd 100644 --- a/DataConnection/CredentialManagement/Services/DataConnectionCredentialService.cs +++ b/DataConnection/CredentialManagement/Services/DataConnectionCredentialService.cs @@ -15,16 +15,16 @@ namespace DataConnection.CredentialManagement.Services; public class DataConnectionCredentialService : IDataConnectionCredentialService { private readonly ICredentialService _credentialService; - private readonly IRecordAssociationService _recordAssociationService; + private readonly IKeyAssociationService _keyAssociationService; private readonly ILogger _logger; public DataConnectionCredentialService( ICredentialService credentialService, - IRecordAssociationService recordAssociationService, + IKeyAssociationService keyAssociationService, ILogger logger) { _credentialService = credentialService; - _recordAssociationService = recordAssociationService; + _keyAssociationService = keyAssociationService; _logger = logger; } @@ -859,66 +859,81 @@ public class DataConnectionCredentialService : IDataConnectionCredentialService #endregion - #region Record Associations + #region Key Associations - public async Task SaveRecordAssociationAsync(RecordAssociation association) + public async Task SaveKeyAssociationAsync(KeyAssociation association) { - return await _recordAssociationService.SaveAssociationAsync(association); + return await _keyAssociationService.SaveAssociationAsync(association); } - public async Task FindRecordAssociationAsync(string sourceName, string sourceKey, string destinationEntity) + public async Task FindKeyAssociationByValueAsync(string keyValue, string destinationEntity, string restCredentialName) { - return await _recordAssociationService.FindAssociationAsync(sourceName, sourceKey, destinationEntity); + return await _keyAssociationService.FindAssociationByKeyValueAsync(keyValue, destinationEntity, restCredentialName); } - public async Task> GetRecordAssociationsBySourceAsync(string sourceName, string sourceType) + public async Task FindKeyAssociationByValueAsync(string keyValue) { - return await _recordAssociationService.GetAssociationsBySourceAsync(sourceName, sourceType); + return await _keyAssociationService.FindAssociationByKeyValueAsync(keyValue); } - public async Task> GetRecordAssociationsByDestinationAsync(string destinationEntity, string restCredentialName) + public async Task> GetKeyAssociationsByDestinationAsync(string destinationEntity, string restCredentialName) { - return await _recordAssociationService.GetAssociationsByDestinationAsync(destinationEntity, restCredentialName); + return await _keyAssociationService.GetAssociationsByDestinationAsync(destinationEntity, restCredentialName); } - public async Task> GetAllActiveRecordAssociationsAsync() + public async Task> GetAllActiveKeyAssociationsAsync() { - return await _recordAssociationService.GetAllActiveAssociationsAsync(); + return await _keyAssociationService.GetAllActiveAssociationsAsync(); } - public async Task UpdateRecordAssociationAsync(RecordAssociation association) + public async Task> GetAllKeyAssociationsAsync() { - return await _recordAssociationService.UpdateAssociationAsync(association); + return await _keyAssociationService.GetAllAssociationsAsync(); } - public async Task DeactivateRecordAssociationAsync(int id) + public async Task UpdateKeyAssociationAsync(KeyAssociation association) { - return await _recordAssociationService.DeactivateAssociationAsync(id); + return await _keyAssociationService.UpdateAssociationAsync(association); } - public async Task DeleteRecordAssociationAsync(int id) + public async Task DeactivateKeyAssociationAsync(int id) { - return await _recordAssociationService.DeleteAssociationAsync(id); + return await _keyAssociationService.DeactivateAssociationAsync(id); } - public async Task ClearRecordAssociationsAsync(string sourceName, string destinationEntity, string restCredentialName) + public async Task DeleteKeyAssociationAsync(int id) { - return await _recordAssociationService.ClearAssociationsAsync(sourceName, destinationEntity, restCredentialName); + return await _keyAssociationService.DeleteAssociationAsync(id); } - public async Task ClearAllRecordAssociationsAsync() + public async Task ClearKeyAssociationsAsync(string destinationEntity, string restCredentialName) { - return await _recordAssociationService.ClearAllAssociationsAsync(); + return await _keyAssociationService.ClearAssociationsAsync(destinationEntity, restCredentialName); } - public async Task> GetInvalidRecordAssociationsAsync(string destinationEntity, string restCredentialName) + public async Task ClearAllKeyAssociationsAsync() { - return await _recordAssociationService.GetInvalidAssociationsAsync(destinationEntity, restCredentialName); + return await _keyAssociationService.ClearAllAssociationsAsync(); } - public async Task CleanupInvalidRecordAssociationsAsync(string destinationEntity, string restCredentialName) + public async Task> GetInvalidKeyAssociationsAsync(string destinationEntity, string restCredentialName) { - return await _recordAssociationService.CleanupInvalidAssociationsAsync(destinationEntity, restCredentialName); + return await _keyAssociationService.GetInvalidAssociationsAsync(destinationEntity, restCredentialName); + } + + public async Task CleanupInvalidKeyAssociationsAsync(string destinationEntity, string restCredentialName) + { + return await _keyAssociationService.CleanupInvalidAssociationsAsync(destinationEntity, restCredentialName); + } + + public async Task UpdateKeyAssociationLastVerifiedAsync(int id) + { + return await _keyAssociationService.UpdateLastVerifiedAsync(id); + } + + public async Task GetKeyAssociationStatisticsAsync() + { + return await _keyAssociationService.GetStatisticsAsync(); } #endregion diff --git a/Data_Coupler/Data_Coupler.csproj b/Data_Coupler/Data_Coupler.csproj index 45dfb68..ab66041 100644 --- a/Data_Coupler/Data_Coupler.csproj +++ b/Data_Coupler/Data_Coupler.csproj @@ -14,6 +14,10 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/Data_Coupler/Pages/DataCoupler.razor b/Data_Coupler/Pages/DataCoupler.razor index 9cc3234..0221175 100644 --- a/Data_Coupler/Pages/DataCoupler.razor +++ b/Data_Coupler/Pages/DataCoupler.razor @@ -834,13 +834,24 @@ @if (useRecordAssociations) { +
+ + Come funziona il nuovo sistema: +
    +
  • Ogni valore di chiave univoco viene associato a un record di destinazione
  • +
  • Più sorgenti diverse possono gestire lo stesso oggetto business usando lo stesso valore chiave
  • +
  • Gli aggiornamenti avvengono automaticamente quando si trova un'associazione esistente
  • +
  • Il sistema individua automaticamente le chiavi dove possibile, ma puoi sempre scegliere manualmente
  • +
+
+
@@ -2126,79 +2137,101 @@ // Genera la chiave sorgente per questo 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)) { - var existingAssociation = await CredentialService.FindRecordAssociationAsync( - currentSourceName, sourceKey, selectedRestEntity.Name); + Logger.LogInformation("ASSOCIATION DEBUG: Cerco associazione - KeyValue: '{KeyValue}', Entity: '{Entity}', Credential: '{Credential}'", + 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) { - // VALIDAZIONE: Verifica se l'ID di destinazione esiste ancora nel sistema target - bool destinationExists = false; + // Prova direttamente l'aggiornamento - più efficiente che verificare prima l'esistenza + Logger.LogInformation("ASSOCIATION DEBUG: Tentativo aggiornamento record esistente - DestinationId: '{DestinationId}'", existingAssociation.DestinationId); + try { - // Usa il campo ID appropriato per cercare l'entità - var idField = GetEntityIdField(); // Potrebbe essere "DocEntry", "id", "Id", etc. - var searchKeys = new Dictionary { { idField, existingAssociation.DestinationId } }; - - var foundEntities = await currentRestClient.FindEntitiesByKeysAsync( - selectedRestEntity.Name, searchKeys); - destinationExists = foundEntities != null && foundEntities.Any(); - } - catch (Exception ex) - { - Logger.LogWarning(ex, "Errore nella verifica dell'esistenza dell'entità {EntityId} - assumo che non esista", existingAssociation.DestinationId); - destinationExists = false; - } - - if (!destinationExists) - { - // L'ID di destinazione non esiste più - elimina l'associazione non valida - Logger.LogWarning("ID destinazione {DestinationId} non più valido per associazione {AssociationId} - eliminazione associazione", - existingAssociation.DestinationId, existingAssociation.Id); + var updateResult = await currentRestClient.UpdateEntityAsync( + selectedRestEntity.Name, existingAssociation.DestinationId, restData); - 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; + if (updateResult != null) + { + 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 updateEx) + { + // Update fallito con eccezione - probabilmente l'entità non esiste più + Logger.LogWarning(updateEx, "ASSOCIATION DEBUG: Aggiornamento fallito per associazione {AssociationId} - elimino associazione e creo nuovo record", existingAssociation.Id); + goto HandleInvalidAssociation; } - // L'ID di destinazione esiste - procedi con l'aggiornamento - var updateResult = await currentRestClient.UpdateEntityAsync( - selectedRestEntity.Name, existingAssociation.DestinationId, restData); - - if (updateResult != null) + HandleInvalidAssociation: + // L'ID di destinazione non esiste più o l'update è fallito - elimina l'associazione non valida + try { - 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 - existingAssociation.UpdatedAt = DateTime.UtcNow; - await CredentialService.UpdateRecordAssociationAsync(existingAssociation); - - Logger.LogDebug("Record aggiornato tramite associazione: {EntityId} per chiave sorgente {SourceKey}", - existingAssociation.DestinationId, sourceKey); + await CredentialService.DeleteKeyAssociationAsync(existingAssociation.Id); + Logger.LogInformation("ASSOCIATION DEBUG: Associazione non valida eliminata: {AssociationId}", existingAssociation.Id); } - else + catch (Exception delEx) { - // 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; + Logger.LogWarning(delEx, "Errore nell'eliminazione dell'associazione non valida {AssociationId}", existingAssociation.Id); } - transferResults.Add(transferResult); - recordNumber++; - continue; + transferResult.Status = "info"; + transferResult.Message = $"Associazione non valida eliminata (aggiornamento fallito) - creazione nuovo record"; + + // 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.Message = "Record inserito con successo"; 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 if (useRecordAssociations && !string.IsNullOrEmpty(sourceKey) && !string.IsNullOrEmpty(transferResult.EntityId)) { try { - var association = new RecordAssociation + // Determina i campi chiave automaticamente + var destinationKeyField = GetEntityIdField(); // Campo chiave nella destinazione + + var association = new KeyAssociation { - SourceName = currentSourceName, - SourceType = selectedSourceType, - SourceKey = sourceKey, + KeyValue = sourceKey, + SourceKeyField = sourceKeyField, + DestinationKeyField = destinationKeyField, DestinationEntity = selectedRestEntity.Name, DestinationId = transferResult.EntityId, RestCredentialName = selectedRestCredential, + CreatedAt = DateTime.UtcNow, + LastVerifiedAt = DateTime.UtcNow, AdditionalInfo = System.Text.Json.JsonSerializer.Serialize(new { TransferDate = DateTime.UtcNow, RecordNumber = recordNumber, - MappingCount = fieldMappings.Count + MappingCount = fieldMappings.Count, + SourceType = selectedSourceType }) }; - await CredentialService.SaveRecordAssociationAsync(association); - Logger.LogDebug("Associazione creata: {SourceKey} -> {DestinationId}", sourceKey, transferResult.EntityId); + Logger.LogInformation("ASSOCIATION DEBUG: Creazione nuova associazione - KeyValue: '{KeyValue}', Entity: '{Entity}', DestinationId: '{DestinationId}', Credential: '{Credential}'", + sourceKey, selectedRestEntity.Name, transferResult.EntityId, selectedRestCredential); + + var associationId = await CredentialService.SaveKeyAssociationAsync(association); + Logger.LogInformation("DEBUG: Associazione salvata con ID: {AssociationId}", associationId); } catch (Exception assocEx) { @@ -2599,7 +2642,8 @@ 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) { diff --git a/Data_Coupler/Pages/RecordAssociations.razor b/Data_Coupler/Pages/KeyAssociations.razor similarity index 50% rename from Data_Coupler/Pages/RecordAssociations.razor rename to Data_Coupler/Pages/KeyAssociations.razor index f585ff3..54452e9 100644 --- a/Data_Coupler/Pages/RecordAssociations.razor +++ b/Data_Coupler/Pages/KeyAssociations.razor @@ -1,27 +1,129 @@ -@page "/record-associations" +@page "/key-associations" @using CredentialManager.Models +@using CredentialManager.Services @using DataConnection.CredentialManagement.Interfaces @using Microsoft.AspNetCore.Components.Forms @using Microsoft.JSInterop @inject IDataConnectionCredentialService CredentialService @inject IJSRuntime JSRuntime -@inject ILogger Logger +@inject ILogger Logger -Associazioni Record +Gestione Associazioni Chiavi
-

Associazioni Record

-

Visualizza e gestisci le associazioni tra record sorgente e destinazione

+

Gestione Associazioni Chiavi

+

+ 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. +

+ + @if (statistics != null) + { +
+
+
+
+
+
+

@statistics.TotalAssociations

+

Totali

+
+
+ +
+
+
+
+
+
+
+
+
+
+

@statistics.ActiveAssociations

+

Attive

+
+
+ +
+
+
+
+
+
+
+
+
+
+

@statistics.InactiveAssociations

+

Disattive

+
+
+ +
+
+
+
+
+
+
+
+
+
+

@statistics.UniqueKeyValues

+

Chiavi Uniche

+
+
+ +
+
+
+
+
+
+
+
+
+
+

@statistics.UniqueDestinationEntities

+

Entità

+
+
+ +
+
+
+
+
+
+
+
+
+
+
@(statistics.OldestAssociation?.ToString("dd/MM/yy") ?? "N/A")
+

Più Vecchia

+
+
+ +
+
+
+
+
+
+ } +
- - + +
@@ -50,36 +152,25 @@
-
-
Pulizia Associazioni
-
- -
-
-
Validazione Associazioni
-
- - -
-
-
-
Esportazione
-
- -
@@ -89,18 +180,17 @@ {
-
- @processingMessage -
+
+ @processingMessage
} @if (!string.IsNullOrEmpty(operationMessage)) { -
- + }
@@ -108,70 +198,6 @@
- -
-
-
-
-
-
-

@filteredAssociations.Count

-

Associazioni Totali

-
-
- -
-
-
-
-
-
-
-
-
-
-

@filteredAssociations.Where(a => a.IsActive).Count()

-

Attive

-
-
- -
-
-
-
-
-
-
-
-
-
-

@filteredAssociations.Where(a => !a.IsActive).Count()

-

Disattivate

-
-
- -
-
-
-
-
-
-
-
-
-
-

@filteredAssociations.Select(a => a.SourceName).Distinct().Count()

-

Sorgenti Diverse

-
-
- -
-
-
-
-
-
- @if (isLoading) { @@ -188,7 +214,7 @@ @if (!allAssociations.Any()) { - Nessuna associazione trovata. Le associazioni vengono create automaticamente durante il trasferimento dati. + Nessuna associazione trovata. Le associazioni vengono create automaticamente durante il trasferimento dati quando il sistema di associazioni è abilitato. } else { @@ -201,7 +227,7 @@
- Associazioni Record + Associazioni Chiavi @filteredAssociations.Count
@@ -210,15 +236,15 @@ - - - + + + - + - + @@ -227,15 +253,13 @@ { @@ -304,116 +325,54 @@ - @if (filteredAssociations.Count > pageSize) + @if (totalPages > 1) { -
SorgenteTipoChiave SorgenteValore ChiaveCampo SorgenteCampo Destinazione Entità Destinazione ID DestinazioneCredenziale RESTCredenziale Stato CreataAggiornataVerificata Azioni
- @association.SourceName + @association.KeyValue - - @association.SourceType - + @association.SourceKeyField - @association.SourceKey + @association.DestinationKeyField @association.DestinationEntity @@ -267,32 +291,29 @@ - @(association.UpdatedAt?.ToString("dd/MM/yyyy HH:mm") ?? "-") + @(association.LastVerifiedAt?.ToString("dd/MM/yyyy HH:mm") ?? "Mai")
@if (association.IsActive) { - } else { - } - + - @if (!string.IsNullOrEmpty(association.AdditionalInfo)) - { - - }
+ + + + + + + + + + + + + @foreach (var assoc in allAssociations) + { + + + + + + + + + + } + +
IDValore ChiaveEntitàID DestinazioneCredenzialeCreataAzioni
@assoc.Id@assoc.KeyValue@assoc.DestinationEntity@assoc.DestinationId@assoc.RestCredentialName@assoc.CreatedAt.ToString("dd/MM/yyyy HH:mm") + +
+
+ } + else if (allAssociations != null) + { +
+ Nessuna associazione trovata. +
+ } +
+
+
+
+ + +@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? 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(); + } +} diff --git a/Data_Coupler/Shared/NavMenu.razor b/Data_Coupler/Shared/NavMenu.razor index 184ffb9..e0bfde3 100644 --- a/Data_Coupler/Shared/NavMenu.razor +++ b/Data_Coupler/Shared/NavMenu.razor @@ -18,11 +18,13 @@ Counter - + + diff --git a/Data_Coupler/wwwroot/data/credentials.db b/Data_Coupler/wwwroot/data/credentials.db index 10621da..aeb0244 100644 Binary files a/Data_Coupler/wwwroot/data/credentials.db and b/Data_Coupler/wwwroot/data/credentials.db differ diff --git a/Data_Coupler/wwwroot/data/credentials.db-shm b/Data_Coupler/wwwroot/data/credentials.db-shm index 028f54c..6262d13 100644 Binary files a/Data_Coupler/wwwroot/data/credentials.db-shm and b/Data_Coupler/wwwroot/data/credentials.db-shm differ diff --git a/Data_Coupler/wwwroot/data/credentials.db-wal b/Data_Coupler/wwwroot/data/credentials.db-wal index 863b848..11e98fa 100644 Binary files a/Data_Coupler/wwwroot/data/credentials.db-wal and b/Data_Coupler/wwwroot/data/credentials.db-wal differ