From b9670ae426c07c16046dd0171a355b4b8ec187d4 Mon Sep 17 00:00:00 2001 From: Alessio Dal Santo Date: Mon, 16 Feb 2026 14:42:03 +0100 Subject: [PATCH] [Feature] Implementato sistema di valori default per campi mapping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Creato modello FieldMappingEntry per gestione unificata di field mapping e default values - Aggiunta colonna DefaultValuesJson alla tabella DataCouplerProfile (max 4000 caratteri) - Implementata UI con toggle per selezionare modalità Mapping o Default - Supporto per 9 tipi di dati: string, int, long, decimal, double, float, boolean, datetime, datetimeoffset - Aggiornata logica TransformRecordToRestEntity per applicare valori default dopo field mapping - Implementata serializzazione/deserializzazione DefaultValues in DataCouplerProfileService - Sistema completo di salvataggio/caricamento valori default nei profili - Migrazione database AddDefaultValuesJsonToProfile creata e applicata --- Components/ProfileSaver.razor.cs | 2 + ..._AddDefaultValuesJsonToProfile.Designer.cs | 601 ++++++++++++++++++ ...216113009_AddDefaultValuesJsonToProfile.cs | 29 + .../CredentialDbContextModelSnapshot.cs | 4 + .../Models/DataCouplerProfile.cs | 5 + .../Models/DataCouplerProfileDto.cs | 14 +- CredentialManager/Models/MappingModels.cs | 174 +++++ .../Services/DataCouplerProfileService.cs | 61 ++ Data_Coupler/Pages/DataCoupler.razor | 229 +++++-- Data_Coupler/Pages/DataCoupler.razor.cs | 195 +++++- 10 files changed, 1249 insertions(+), 65 deletions(-) create mode 100644 CredentialManager/Data/Migrations/20260216113009_AddDefaultValuesJsonToProfile.Designer.cs create mode 100644 CredentialManager/Data/Migrations/20260216113009_AddDefaultValuesJsonToProfile.cs diff --git a/Components/ProfileSaver.razor.cs b/Components/ProfileSaver.razor.cs index d51cb09..aa61983 100644 --- a/Components/ProfileSaver.razor.cs +++ b/Components/ProfileSaver.razor.cs @@ -25,6 +25,7 @@ public partial class ProfileSaver [Parameter] public string? DestinationTable { get; set; } [Parameter] public string? DestinationEndpoint { get; set; } [Parameter] public List? FieldMappings { get; set; } + [Parameter] public Dictionary? DefaultValues { get; set; } [Parameter] public List? ExternalIdRelationships { get; set; } [Parameter] public string? SourceKeyField { get; set; } [Parameter] public bool UseRecordAssociations { get; set; } @@ -79,6 +80,7 @@ public partial class ProfileSaver DestinationTable = DestinationTable, DestinationEndpoint = DestinationEndpoint, FieldMappings = FieldMappings, + DefaultValues = DefaultValues, ExternalIdRelationships = ExternalIdRelationships, SourceKeyField = SourceKeyField, UseRecordAssociations = UseRecordAssociations diff --git a/CredentialManager/Data/Migrations/20260216113009_AddDefaultValuesJsonToProfile.Designer.cs b/CredentialManager/Data/Migrations/20260216113009_AddDefaultValuesJsonToProfile.Designer.cs new file mode 100644 index 0000000..7ba2911 --- /dev/null +++ b/CredentialManager/Data/Migrations/20260216113009_AddDefaultValuesJsonToProfile.Designer.cs @@ -0,0 +1,601 @@ +// +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.Data.Migrations +{ + [DbContext(typeof(CredentialDbContext))] + [Migration("20260216113009_AddDefaultValuesJsonToProfile")] + partial class AddDefaultValuesJsonToProfile + { + /// + 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("OdbcDsnName") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OdbcMode") + .HasMaxLength(20) + .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.DataCouplerProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("CreatedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DefaultValuesJson") + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("DeletionAction") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("DeletionMarkField") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DeletionMarkValue") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DestinationCredentialId") + .HasColumnType("INTEGER"); + + b.Property("DestinationEndpoint") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DestinationSchema") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DestinationTable") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DestinationType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("ExternalIdRelationshipsJson") + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("FieldMappingJson") + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("LastUsedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SourceCredentialId") + .HasColumnType("INTEGER"); + + b.Property("SourceCustomQuery") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("SourceDatabaseName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SourceFilePath") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("SourceKeyField") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SourceSchema") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SourceTable") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SourceType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("SyncDeletions") + .HasColumnType("INTEGER"); + + b.Property("UseRecordAssociations") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("DestinationCredentialId"); + + b.HasIndex("DestinationType"); + + b.HasIndex("IsActive"); + + b.HasIndex("LastUsedAt"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("SourceCredentialId"); + + b.HasIndex("SourceType"); + + b.ToTable("DataCouplerProfiles", (string)null); + }); + + modelBuilder.Entity("CredentialManager.Models.KeyAssociation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdditionalInfo") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Data_Hash") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("DeletionSynced") + .HasColumnType("INTEGER"); + + b.Property("DeletionSyncedAt") + .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("IsSourceDeleted") + .HasColumnType("INTEGER"); + + b.Property("KeyValue") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("LastVerifiedAt") + .HasColumnType("TEXT"); + + b.Property("MappedDestinationField") + .HasMaxLength(200) + .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); + }); + + modelBuilder.Entity("CredentialManager.Models.ProfileSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DailyTime") + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("DayOfMonth") + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DestinationDatabaseOverride") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("EnableDeletionSync") + .HasColumnType("INTEGER"); + + b.Property("ExecutionCount") + .HasColumnType("INTEGER"); + + b.Property("IntervalUnit") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("IntervalValue") + .HasColumnType("INTEGER"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("LastExecutionMessage") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("LastExecutionRecordCount") + .HasColumnType("INTEGER"); + + b.Property("LastExecutionStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("LastExecutionTime") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("NextExecutionTime") + .HasColumnType("TEXT"); + + b.Property("ProfileId") + .HasColumnType("INTEGER"); + + b.Property("ScheduleType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("ScheduledDateTime") + .HasColumnType("TEXT"); + + b.Property("SourceDatabaseOverride") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProfileId"); + + b.ToTable("ProfileSchedules"); + }); + + modelBuilder.Entity("CredentialManager.Models.ScheduleExecutionHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdditionalInfo") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DestinationInfo") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DestinationType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("EndTime") + .HasColumnType("TEXT"); + + b.Property("ErrorDetails") + .HasMaxLength(5000) + .HasColumnType("TEXT"); + + b.Property("Message") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("ProfileId") + .HasColumnType("INTEGER"); + + b.Property("ProfileName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RecordsProcessed") + .HasColumnType("INTEGER"); + + b.Property("RecordsWithErrors") + .HasColumnType("INTEGER"); + + b.Property("ScheduleId") + .HasColumnType("INTEGER"); + + b.Property("SourceInfo") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("SourceType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("TriggerType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("TriggeredBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProfileId"); + + b.HasIndex("ScheduleId"); + + b.HasIndex("StartTime"); + + b.HasIndex("Status"); + + b.HasIndex("TriggerType"); + + b.ToTable("ScheduleExecutionHistories", (string)null); + }); + + modelBuilder.Entity("CredentialManager.Models.DataCouplerProfile", b => + { + b.HasOne("CredentialManager.Models.CredentialEntity", "DestinationCredential") + .WithMany() + .HasForeignKey("DestinationCredentialId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("CredentialManager.Models.CredentialEntity", "SourceCredential") + .WithMany() + .HasForeignKey("SourceCredentialId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("DestinationCredential"); + + b.Navigation("SourceCredential"); + }); + + modelBuilder.Entity("CredentialManager.Models.ProfileSchedule", b => + { + b.HasOne("CredentialManager.Models.DataCouplerProfile", "Profile") + .WithMany() + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("CredentialManager.Models.ScheduleExecutionHistory", b => + { + b.HasOne("CredentialManager.Models.ProfileSchedule", "Schedule") + .WithMany() + .HasForeignKey("ScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Schedule"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CredentialManager/Data/Migrations/20260216113009_AddDefaultValuesJsonToProfile.cs b/CredentialManager/Data/Migrations/20260216113009_AddDefaultValuesJsonToProfile.cs new file mode 100644 index 0000000..1ff1a0e --- /dev/null +++ b/CredentialManager/Data/Migrations/20260216113009_AddDefaultValuesJsonToProfile.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CredentialManager.Data.Migrations +{ + /// + public partial class AddDefaultValuesJsonToProfile : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DefaultValuesJson", + table: "DataCouplerProfiles", + type: "TEXT", + maxLength: 4000, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "DefaultValuesJson", + table: "DataCouplerProfiles"); + } + } +} diff --git a/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs b/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs index 85a94f5..a8a6783 100644 --- a/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs +++ b/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs @@ -146,6 +146,10 @@ namespace CredentialManager.Migrations .HasMaxLength(100) .HasColumnType("TEXT"); + b.Property("DefaultValuesJson") + .HasMaxLength(4000) + .HasColumnType("TEXT"); + b.Property("DeletionAction") .HasMaxLength(20) .HasColumnType("TEXT"); diff --git a/CredentialManager/Models/DataCouplerProfile.cs b/CredentialManager/Models/DataCouplerProfile.cs index 8b53659..cbb8b14 100644 --- a/CredentialManager/Models/DataCouplerProfile.cs +++ b/CredentialManager/Models/DataCouplerProfile.cs @@ -60,6 +60,11 @@ public class DataCouplerProfile [MaxLength(4000)] public string? FieldMappingJson { get; set; } + // Default values per i campi di destinazione salvati come JSON + // Formato: { "DestinationField": { "Value": "defaultValue", "Type": "string" } } + [MaxLength(4000)] + public string? DefaultValuesJson { get; set; } + // External ID Relationships per Salesforce salvate come JSON [MaxLength(4000)] public string? ExternalIdRelationshipsJson { get; set; } diff --git a/CredentialManager/Models/DataCouplerProfileDto.cs b/CredentialManager/Models/DataCouplerProfileDto.cs index a73b1ec..f3d685c 100644 --- a/CredentialManager/Models/DataCouplerProfileDto.cs +++ b/CredentialManager/Models/DataCouplerProfileDto.cs @@ -30,6 +30,9 @@ public class DataCouplerProfileDto // Mapping dei campi public List? FieldMappings { get; set; } + // Default values per campi destinazione (FieldName -> (Value, Type)) + public Dictionary? DefaultValues { get; set; } + // External ID Relationships per Salesforce public List? ExternalIdRelationships { get; set; } @@ -83,8 +86,15 @@ public class ExternalIdRelationshipDto public string SourceField { get; set; } = string.Empty; } -/// -/// DTO per la visualizzazione di un profilo nella lista +/// /// DTO per i valori di default +/// +public class DefaultValueDto +{ + public object? Value { get; set; } + public string? Type { get; set; } +} + +/// /// DTO per la visualizzazione di un profilo nella lista /// public class DataCouplerProfileSummaryDto { diff --git a/CredentialManager/Models/MappingModels.cs b/CredentialManager/Models/MappingModels.cs index e69de29..d674a48 100644 --- a/CredentialManager/Models/MappingModels.cs +++ b/CredentialManager/Models/MappingModels.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace CredentialManager.Models +{ + /// + /// Tipo di mapping field + /// + public enum MappingType + { + /// + /// Mapping da campo sorgente a campo destinazione + /// + FieldMapping, + + /// + /// Valore di default per campo destinazione + /// + DefaultValue + } + + /// + /// Rappresenta una voce di mapping che può essere: + /// - Un mapping da campo sorgente a campo destinazione + /// - Un valore di default per un campo destinazione + /// + public class FieldMappingEntry + { + /// + /// Tipo di mapping + /// + [JsonPropertyName("type")] + public MappingType Type { get; set; } + + /// + /// Nome del campo sorgente (solo per FieldMapping) + /// + [JsonPropertyName("sourceField")] + public string? SourceField { get; set; } + + /// + /// Nome del campo destinazione + /// + [JsonPropertyName("destinationField")] + public string DestinationField { get; set; } = string.Empty; + + /// + /// Valore di default (solo per DefaultValue) + /// + [JsonPropertyName("defaultValue")] + public object? DefaultValue { get; set; } + + /// + /// Tipo di dato del valore di default (per conversioni corrette) + /// Esempi: "string", "int", "decimal", "boolean", "datetime" + /// + [JsonPropertyName("defaultValueType")] + public string? DefaultValueType { get; set; } + + /// + /// Crea un mapping da campo sorgente a campo destinazione + /// + public static FieldMappingEntry CreateFieldMapping(string sourceField, string destinationField) + { + return new FieldMappingEntry + { + Type = MappingType.FieldMapping, + SourceField = sourceField, + DestinationField = destinationField + }; + } + + /// + /// Crea un valore di default per un campo destinazione + /// + public static FieldMappingEntry CreateDefaultValue(string destinationField, object defaultValue, string? valueType = null) + { + return new FieldMappingEntry + { + Type = MappingType.DefaultValue, + DestinationField = destinationField, + DefaultValue = defaultValue, + DefaultValueType = valueType ?? InferValueType(defaultValue) + }; + } + + /// + /// Determina automaticamente il tipo del valore + /// + private static string InferValueType(object? value) + { + if (value == null) return "string"; + + return value switch + { + string _ => "string", + int _ => "int", + long _ => "long", + decimal _ => "decimal", + double _ => "double", + float _ => "float", + bool _ => "boolean", + DateTime _ => "datetime", + DateTimeOffset _ => "datetimeoffset", + _ => "string" + }; + } + + /// + /// Ottiene una descrizione user-friendly del mapping + /// + public string GetDescription() + { + return Type switch + { + MappingType.FieldMapping => $"{SourceField} → {DestinationField}", + MappingType.DefaultValue => $"{DestinationField} = {DefaultValue ?? "null"} ({DefaultValueType})", + _ => "Unknown" + }; + } + } + + /// + /// Helper per la conversione tra vecchio formato (Dictionary) e nuovo formato (FieldMappingEntry) + /// + public static class MappingConverter + { + /// + /// Converte il vecchio formato Dictionary in lista di FieldMappingEntry + /// + public static List FromDictionary(Dictionary oldMappings) + { + var entries = new List(); + + foreach (var mapping in oldMappings) + { + entries.Add(FieldMappingEntry.CreateFieldMapping(mapping.Key, mapping.Value)); + } + + return entries; + } + + /// + /// Converte una lista di FieldMappingEntry nel vecchio formato Dictionary (solo field mappings) + /// + public static Dictionary ToDictionary(List entries) + { + var dictionary = new Dictionary(); + + foreach (var entry in entries.Where(e => e.Type == MappingType.FieldMapping && !string.IsNullOrEmpty(e.SourceField))) + { + dictionary[entry.SourceField!] = entry.DestinationField; + } + + return dictionary; + } + + /// + /// Ottiene solo i valori di default da una lista di entries + /// + public static Dictionary GetDefaultValues(List entries) + { + var defaults = new Dictionary(); + + foreach (var entry in entries.Where(e => e.Type == MappingType.DefaultValue)) + { + defaults[entry.DestinationField] = (entry.DefaultValue, entry.DefaultValueType); + } + + return defaults; + } + } +} diff --git a/CredentialManager/Services/DataCouplerProfileService.cs b/CredentialManager/Services/DataCouplerProfileService.cs index 00564ac..6001bfc 100644 --- a/CredentialManager/Services/DataCouplerProfileService.cs +++ b/CredentialManager/Services/DataCouplerProfileService.cs @@ -216,6 +216,7 @@ public class DataCouplerProfileService : IDataCouplerProfileService }); } + /// /// /// Deserializza il JSON delle External ID Relationships /// @@ -236,6 +237,64 @@ public class DataCouplerProfileService : IDataCouplerProfileService return new List(); } } + + /// + /// Serializza i default values in JSON + /// + public string SerializeDefaultValues(Dictionary? defaultValues) + { + if (defaultValues == null || !defaultValues.Any()) + return string.Empty; + + // Converti in un formato serializzabile (Dictionary) + var serializable = new Dictionary(); + foreach (var entry in defaultValues) + { + serializable[entry.Key] = new DefaultValueDto + { + Value = entry.Value.Value, + Type = entry.Value.Type + }; + } + + return JsonSerializer.Serialize(serializable, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + } + + /// + /// Deserializza il JSON dei default values + /// + public Dictionary DeserializeDefaultValues(string? json) + { + if (string.IsNullOrWhiteSpace(json)) + return new Dictionary(); + + try + { + var deserialized = JsonSerializer.Deserialize>(json, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + + if (deserialized == null) + return new Dictionary(); + + // Converti nel formato tuple + var result = new Dictionary(); + foreach (var entry in deserialized) + { + result[entry.Key] = (entry.Value.Value, entry.Value.Type); + } + + return result; + } + catch + { + return new Dictionary(); + } + } /// /// Converte un DataCouplerProfile in DTO @@ -262,6 +321,7 @@ public class DataCouplerProfileService : IDataCouplerProfileService DestinationTable = profile.DestinationTable, DestinationEndpoint = profile.DestinationEndpoint, FieldMappings = DeserializeFieldMappings(profile.FieldMappingJson), + DefaultValues = DeserializeDefaultValues(profile.DefaultValuesJson), ExternalIdRelationships = DeserializeExternalIdRelationships(profile.ExternalIdRelationshipsJson), SourceKeyField = profile.SourceKeyField, UseRecordAssociations = profile.UseRecordAssociations @@ -291,6 +351,7 @@ public class DataCouplerProfileService : IDataCouplerProfileService DestinationTable = dto.DestinationTable, DestinationEndpoint = dto.DestinationEndpoint, FieldMappingJson = SerializeFieldMappings(dto.FieldMappings), + DefaultValuesJson = SerializeDefaultValues(dto.DefaultValues), ExternalIdRelationshipsJson = SerializeExternalIdRelationships(dto.ExternalIdRelationships), SourceKeyField = dto.SourceKeyField, UseRecordAssociations = dto.UseRecordAssociations, diff --git a/Data_Coupler/Pages/DataCoupler.razor b/Data_Coupler/Pages/DataCoupler.razor index eaf9a2c..9d956f8 100644 --- a/Data_Coupler/Pages/DataCoupler.razor +++ b/Data_Coupler/Pages/DataCoupler.razor @@ -920,23 +920,80 @@
- - - + +
+ + +
+ + + @if (!isAddingDefaultValue) + { + + + + } + else + { + +
+ Tipo Valore: + + + + @if (defaultValueType == "datetime") + { + Es: @DateTime.Now.ToString("yyyy-MM-dd") + } + else if (defaultValueType == "boolean") + { + Es: true o false + } + else if (defaultValueType == "decimal") + { + Es: 100.50 + } + +
+ + } +
@@ -965,6 +1022,10 @@ { Mapped } + @if (defaultValues.ContainsKey(property.Name)) + { + Default + } @@ -1087,11 +1148,11 @@ } - @if (fieldMappings.Any()) + @if (fieldMappings.Any() || defaultValues.Any()) {
-
Mappature Correnti (@fieldMappings.Count)
+
Configurazione Mapping (@(fieldMappings.Count + defaultValues.Count) totali)
@if (keyFields.Any()) { @@ -1099,44 +1160,101 @@ }
-
- - - - - - - - - - - - - @foreach (var mapping in fieldMappings) - { - DbColumnInfo? dbColumn = null; - if (selectedSourceType == "database" && !string.IsNullOrEmpty(selectedTable)) - { - dbColumn = databaseTables.ContainsKey(selectedTable) ? - databaseTables[selectedTable].FirstOrDefault(c => c.Name == mapping.Key) : null; - } - var restProperty = restEntityDetails?.Properties.FirstOrDefault(p => p.Name == mapping.Value); - - - - - - - - - } - -
Campo DatabaseTipo DBProprietà RESTTipo RESTAzioni
@mapping.Key@(dbColumn?.DataType ?? (selectedSourceType == "file" ? "Text" : "Unknown"))@mapping.Value@(restProperty?.Type ?? "Unknown") - -
-
+ + + @if (fieldMappings.Any()) + { +
+
+ Field Mappings (@fieldMappings.Count) +
+
+
+ + + + + + + + + + + + + @foreach (var mapping in fieldMappings) + { + DbColumnInfo? dbColumn = null; + if (selectedSourceType == "database" && !string.IsNullOrEmpty(selectedTable)) + { + dbColumn = databaseTables.ContainsKey(selectedTable) ? + databaseTables[selectedTable].FirstOrDefault(c => c.Name == mapping.Key) : null; + } + var restProperty = restEntityDetails?.Properties.FirstOrDefault(p => p.Name == mapping.Value); + + + + + + + + + } + +
Campo SorgenteTipo SorgenteCampo DestinazioneTipo DestinazioneAzioni
@mapping.Key@(dbColumn?.DataType ?? (selectedSourceType == "file" ? "Text" : "Unknown"))@mapping.Value@(restProperty?.Type ?? "Unknown") + +
+
+
+
+ } + + + @if (defaultValues.Any()) + { +
+
+ Default Values (@defaultValues.Count) +
+
+
+ + + + + + + + + + + + @foreach (var defaultValue in defaultValues) + { + var restProperty = restEntityDetails?.Properties.FirstOrDefault(p => p.Name == defaultValue.Key); + var (value, valueType) = defaultValue.Value; + + + + + + + + } + +
Campo DestinazioneValore DefaultTipo ValoreTipo Campo RESTAzioni
@defaultValue.Key@(value?.ToString() ?? "null") + @valueType + @(restProperty?.Type ?? "Unknown") + +
+
+
+
+ }
} @@ -1314,6 +1432,7 @@ DestinationCredentialName="@selectedRestCredential" DestinationEndpoint="@selectedRestEntity?.Name" FieldMappings="@GetCurrentFieldMappings()" + DefaultValues="@defaultValues" ExternalIdRelationships="@externalIdRelationships" SourceKeyField="@sourceKeyField" UseRecordAssociations="@useRecordAssociations" diff --git a/Data_Coupler/Pages/DataCoupler.razor.cs b/Data_Coupler/Pages/DataCoupler.razor.cs index 2e481b7..0595765 100644 --- a/Data_Coupler/Pages/DataCoupler.razor.cs +++ b/Data_Coupler/Pages/DataCoupler.razor.cs @@ -51,10 +51,18 @@ public partial class DataCoupler : ComponentBase (int)Math.Ceiling((double)fileData[sheetName].Count / pageSize) : 0; // Mapping campi - private Dictionary fieldMappings = new(); // DbColumn -> RestProperty + private Dictionary fieldMappings = new(); // DbColumn -> RestProperty (legacy) + private List fieldMappingEntries = new(); // New system: supporta sia mapping che default values + private Dictionary defaultValues = new(); // DestinationField -> (DefaultValue, Type) private HashSet keyFields = new(); // REST properties marked as keys private string selectedDbColumn = ""; + // UI per configurazione mapping/default value + private bool isAddingDefaultValue = false; // Toggle tra mapping normale e default value + private string defaultValueField = ""; // Campo destinazione per default value + private string defaultValueInput = ""; // Input utente per default value + private string defaultValueType = "string"; // Tipo del default value (string, int, decimal, boolean, datetime) + // External ID Relationships (Salesforce) private List externalIdRelationships = new(); private string selectedRelationshipObject = ""; @@ -345,11 +353,13 @@ public partial class DataCoupler : ComponentBase // Applica i mapping fieldMappings.Clear(); + fieldMappingEntries.Clear(); keyFields.Clear(); foreach (var mapping in mappings) { fieldMappings[mapping.SourceField] = mapping.DestinationField; + fieldMappingEntries.Add(FieldMappingEntry.CreateFieldMapping(mapping.SourceField, mapping.DestinationField)); if (mapping.IsKey) { keyFields.Add(mapping.DestinationField); @@ -370,6 +380,42 @@ public partial class DataCoupler : ComponentBase { Logger.LogInformation("Nessun mapping campi da applicare"); } + + // Step 4.5: Applica default values se disponibili + if (!string.IsNullOrEmpty(profile.DefaultValuesJson)) + { + Logger.LogInformation("Step 4.5 - Applicazione default values..."); + try + { + var deserializedDefaults = System.Text.Json.JsonSerializer.Deserialize>( + profile.DefaultValuesJson, + new System.Text.Json.JsonSerializerOptions { PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase }); + + if (deserializedDefaults != null) + { + defaultValues.Clear(); + + foreach (var entry in deserializedDefaults) + { + defaultValues[entry.Key] = (entry.Value.Value, entry.Value.Type); + fieldMappingEntries.Add(FieldMappingEntry.CreateDefaultValue(entry.Key, entry.Value.Value, entry.Value.Type)); + + Logger.LogInformation("Default value applicato: {Field} = {Value} ({Type})", + entry.Key, entry.Value.Value, entry.Value.Type); + } + + Logger.LogInformation("Default values applicati - Totale: {Count}", defaultValues.Count); + } + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Errore nel caricamento dei default values dal profilo"); + } + } + else + { + Logger.LogInformation("Nessun default value da applicare"); + } // Step 5: Applica configurazione chiave sorgente if (!string.IsNullOrEmpty(profile.SourceKeyField)) @@ -721,6 +767,8 @@ public partial class DataCoupler : ComponentBase ResetSourceState(); ResetDestinationState(); fieldMappings.Clear(); + fieldMappingEntries.Clear(); + defaultValues.Clear(); keyFields.Clear(); externalIdRelationships.Clear(); // Reset relazioni transferResults.Clear(); @@ -1328,6 +1376,17 @@ public partial class DataCoupler : ComponentBase // Crea il nuovo mapping fieldMappings[selectedDbColumn] = selectedRestProperty; + // Aggiorna anche la lista FieldMappingEntries + var existingEntry = fieldMappingEntries.FirstOrDefault(e => + e.Type == CredentialManager.Models.MappingType.FieldMapping && e.SourceField == selectedDbColumn); + + if (existingEntry != null) + { + fieldMappingEntries.Remove(existingEntry); + } + + fieldMappingEntries.Add(FieldMappingEntry.CreateFieldMapping(selectedDbColumn, selectedRestProperty)); + Logger.LogInformation("Creato mapping: {DbColumn} -> {RestProperty}", selectedDbColumn, selectedRestProperty); // Deseleziona i campi @@ -1335,14 +1394,108 @@ public partial class DataCoupler : ComponentBase selectedRestProperty = ""; } + private void CreateDefaultValue() + { + if (string.IsNullOrEmpty(selectedRestProperty) || string.IsNullOrEmpty(defaultValueInput)) + return; + + try + { + // Converti il valore nel tipo appropriato + object? convertedValue = ConvertDefaultValue(defaultValueInput, defaultValueType); + + // Rimuovi eventuale default value esistente per questo campo + if (defaultValues.ContainsKey(selectedRestProperty)) + { + defaultValues.Remove(selectedRestProperty); + } + + // Rimuovi anche dalla lista entries + var existingEntry = fieldMappingEntries.FirstOrDefault(e => + e.Type == CredentialManager.Models.MappingType.DefaultValue && e.DestinationField == selectedRestProperty); + + if (existingEntry != null) + { + fieldMappingEntries.Remove(existingEntry); + } + + // Aggiungi il nuovo default value + defaultValues[selectedRestProperty] = (convertedValue, defaultValueType); + fieldMappingEntries.Add(FieldMappingEntry.CreateDefaultValue(selectedRestProperty, convertedValue, defaultValueType)); + + Logger.LogInformation("Creato default value: {RestProperty} = {Value} ({Type})", + selectedRestProperty, convertedValue, defaultValueType); + + // Reset campi + selectedRestProperty = ""; + defaultValueInput = ""; + isAddingDefaultValue = false; + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nella conversione del valore di default"); + transferMessage = $"Errore: {ex.Message}"; + transferMessageType = "error"; + } + } + + private object? ConvertDefaultValue(string input, string type) + { + if (string.IsNullOrEmpty(input)) + return null; + + return type.ToLower() switch + { + "string" => input, + "int" => int.Parse(input), + "long" => long.Parse(input), + "decimal" => decimal.Parse(input, System.Globalization.CultureInfo.InvariantCulture), + "double" => double.Parse(input, System.Globalization.CultureInfo.InvariantCulture), + "float" => float.Parse(input, System.Globalization.CultureInfo.InvariantCulture), + "boolean" => bool.Parse(input), + "datetime" => DateTime.Parse(input), + "datetimeoffset" => DateTimeOffset.Parse(input), + _ => input + }; + } + private void RemoveMapping() { if (string.IsNullOrEmpty(selectedDbColumn) || !fieldMappings.ContainsKey(selectedDbColumn)) return; fieldMappings.Remove(selectedDbColumn); + + // Rimuovi anche dalla lista entries + var entry = fieldMappingEntries.FirstOrDefault(e => + e.Type == CredentialManager.Models.MappingType.FieldMapping && e.SourceField == selectedDbColumn); + if (entry != null) + { + fieldMappingEntries.Remove(entry); + } + Logger.LogInformation("Rimosso mapping per campo: {DbColumn}", selectedDbColumn); } + + private void RemoveDefaultValue(string destinationField) + { + if (defaultValues.ContainsKey(destinationField)) + { + defaultValues.Remove(destinationField); + + // Rimuovi anche dalla lista entries + var entry = fieldMappingEntries.FirstOrDefault(e => + e.Type == CredentialManager.Models.MappingType.DefaultValue && e.DestinationField == destinationField); + if (entry != null) + { + fieldMappingEntries.Remove(entry); + } + + Logger.LogInformation("Rimosso default value per campo: {Field}", destinationField); + StateHasChanged(); + } + } + private void RemoveSpecificMapping(string dbColumn) { if (fieldMappings.ContainsKey(dbColumn)) @@ -1351,20 +1504,22 @@ public partial class DataCoupler : ComponentBase Logger.LogInformation("Rimosso mapping specifico per campo: {DbColumn}", dbColumn); } } - Logger.LogInformation("Rimosso mapping specifico per campo: {DbColumn}", dbColumn); - } - } private void ClearAllMappings() { fieldMappings.Clear(); + fieldMappingEntries.Clear(); + defaultValues.Clear(); selectedDbColumn = ""; selectedRestProperty = ""; sourceKeyField = ""; transferMessage = ""; transferMessageType = ""; + isAddingDefaultValue = false; + defaultValueField = ""; + defaultValueInput = ""; externalIdRelationships.Clear(); // Pulisce anche le relazioni - Logger.LogInformation("Tutti i mapping e le configurazioni sono stati cancellati"); + Logger.LogInformation("Tutti i mapping, default values e le configurazioni sono stati cancellati"); } // External ID Relationships Methods @@ -2108,6 +2263,7 @@ public partial class DataCoupler : ComponentBase .Select(r => r.SourceField) .ToHashSet(); + // STEP 1: Applica i mapping normali (campo sorgente -> campo destinazione) foreach (var mapping in fieldMappings) { string dbColumn = mapping.Key; @@ -2134,7 +2290,29 @@ public partial class DataCoupler : ComponentBase } } - // Aggiungi External ID Relationships (per Salesforce) + // STEP 2: Applica i valori di default per i campi NON ancora popolati + foreach (var defaultValue in defaultValues) + { + string destinationField = defaultValue.Key; + var (value, valueType) = defaultValue.Value; + + // Applica il default value solo se il campo non è già stato popolato dal mapping + if (!restData.ContainsKey(destinationField)) + { + if (value != null) + { + restData[destinationField] = value; + Logger.LogDebug("Applicato default value: {Field} = {Value} ({Type})", + destinationField, value, valueType); + } + } + else + { + Logger.LogDebug("Campo {Field} già popolato da mapping, default value ignorato", destinationField); + } + } + + // STEP 3: Aggiungi External ID Relationships (per Salesforce) if (externalIdRelationships.Any()) { foreach (var relationship in externalIdRelationships) @@ -2163,9 +2341,10 @@ public partial class DataCoupler : ComponentBase } } - Logger.LogDebug("Record trasformato: {DbColumns} → {RestProperties}", + Logger.LogDebug("Record trasformato: {DbColumns} → {RestProperties} (inclusi {DefaultCount} default values)", string.Join(", ", dbRecord.Keys), - string.Join(", ", restData.Keys)); + string.Join(", ", restData.Keys), + defaultValues.Count(dv => restData.ContainsKey(dv.Key))); return restData; }