Compare commits
3 Commits
3a1c8da3cd
..
v2.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
| b9670ae426 | |||
| 483eb7b407 | |||
| ed5316fbdf |
@@ -25,6 +25,8 @@ public partial class ProfileSaver
|
|||||||
[Parameter] public string? DestinationTable { get; set; }
|
[Parameter] public string? DestinationTable { get; set; }
|
||||||
[Parameter] public string? DestinationEndpoint { get; set; }
|
[Parameter] public string? DestinationEndpoint { get; set; }
|
||||||
[Parameter] public List<FieldMappingDto>? FieldMappings { get; set; }
|
[Parameter] public List<FieldMappingDto>? FieldMappings { get; set; }
|
||||||
|
[Parameter] public Dictionary<string, (object? Value, string? Type)>? DefaultValues { get; set; }
|
||||||
|
[Parameter] public List<ExternalIdRelationshipDto>? ExternalIdRelationships { get; set; }
|
||||||
[Parameter] public string? SourceKeyField { get; set; }
|
[Parameter] public string? SourceKeyField { get; set; }
|
||||||
[Parameter] public bool UseRecordAssociations { get; set; }
|
[Parameter] public bool UseRecordAssociations { get; set; }
|
||||||
[Parameter] public EventCallback<DataCouplerProfileDto> OnProfileSaved { get; set; }
|
[Parameter] public EventCallback<DataCouplerProfileDto> OnProfileSaved { get; set; }
|
||||||
@@ -78,6 +80,8 @@ public partial class ProfileSaver
|
|||||||
DestinationTable = DestinationTable,
|
DestinationTable = DestinationTable,
|
||||||
DestinationEndpoint = DestinationEndpoint,
|
DestinationEndpoint = DestinationEndpoint,
|
||||||
FieldMappings = FieldMappings,
|
FieldMappings = FieldMappings,
|
||||||
|
DefaultValues = DefaultValues,
|
||||||
|
ExternalIdRelationships = ExternalIdRelationships,
|
||||||
SourceKeyField = SourceKeyField,
|
SourceKeyField = SourceKeyField,
|
||||||
UseRecordAssociations = UseRecordAssociations
|
UseRecordAssociations = UseRecordAssociations
|
||||||
};
|
};
|
||||||
|
|||||||
+597
@@ -0,0 +1,597 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using CredentialManager.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace CredentialManager.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(CredentialDbContext))]
|
||||||
|
[Migration("20260215151630_AddExternalIdRelationships")]
|
||||||
|
partial class AddExternalIdRelationships
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "9.0.6");
|
||||||
|
|
||||||
|
modelBuilder.Entity("CredentialManager.Models.CredentialEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AdditionalParameters")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("CommandTimeout")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(30);
|
||||||
|
|
||||||
|
b.Property<string>("ConnectionString")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("CreatedBy")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DatabaseName")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DatabaseType")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptedApiKey")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptedAuthToken")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptedPassword")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Headers")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Host")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IgnoreSslErrors")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(true);
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("OdbcDsnName")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("OdbcMode")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("Port")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("RestServiceType")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("TimeoutSeconds")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(100);
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("DatabaseType");
|
||||||
|
|
||||||
|
b.HasIndex("IsActive");
|
||||||
|
|
||||||
|
b.HasIndex("Name")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("Type");
|
||||||
|
|
||||||
|
b.ToTable("Credentials", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CredentialManager.Models.DataCouplerProfile", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<string>("CreatedBy")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DeletionAction")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DeletionMarkField")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DeletionMarkValue")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("DestinationCredentialId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationEndpoint")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationSchema")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationTable")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ExternalIdRelationshipsJson")
|
||||||
|
.HasMaxLength(4000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("FieldMappingJson")
|
||||||
|
.HasMaxLength(4000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(true);
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastUsedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("SourceCredentialId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("SourceCustomQuery")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SourceDatabaseName")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SourceFilePath")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SourceKeyField")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SourceSchema")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SourceTable")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SourceType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("SyncDeletions")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("UseRecordAssociations")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedAt");
|
||||||
|
|
||||||
|
b.HasIndex("DestinationCredentialId");
|
||||||
|
|
||||||
|
b.HasIndex("DestinationType");
|
||||||
|
|
||||||
|
b.HasIndex("IsActive");
|
||||||
|
|
||||||
|
b.HasIndex("LastUsedAt");
|
||||||
|
|
||||||
|
b.HasIndex("Name")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("SourceCredentialId");
|
||||||
|
|
||||||
|
b.HasIndex("SourceType");
|
||||||
|
|
||||||
|
b.ToTable("DataCouplerProfiles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CredentialManager.Models.KeyAssociation", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AdditionalInfo")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Data_Hash")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("DeletionSynced")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletionSyncedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationEntity")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationKeyField")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(true);
|
||||||
|
|
||||||
|
b.Property<bool>("IsSourceDeleted")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("KeyValue")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastVerifiedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("MappedDestinationField")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("RestCredentialName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SourceKeyField")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SourcesInfo")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedAt");
|
||||||
|
|
||||||
|
b.HasIndex("DestinationEntity");
|
||||||
|
|
||||||
|
b.HasIndex("IsActive");
|
||||||
|
|
||||||
|
b.HasIndex("KeyValue")
|
||||||
|
.HasDatabaseName("IX_KeyAssociations_KeyValue");
|
||||||
|
|
||||||
|
b.HasIndex("LastVerifiedAt");
|
||||||
|
|
||||||
|
b.HasIndex("RestCredentialName");
|
||||||
|
|
||||||
|
b.HasIndex("KeyValue", "DestinationEntity", "RestCredentialName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("IX_KeyAssociations_Unique");
|
||||||
|
|
||||||
|
b.ToTable("KeyAssociations", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CredentialManager.Models.ProfileSchedule", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("CreatedBy")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DailyTime")
|
||||||
|
.HasMaxLength(10)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("DayOfMonth")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("DayOfWeek")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationDatabaseOverride")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("EnableDeletionSync")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ExecutionCount")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("IntervalUnit")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("IntervalValue")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("LastExecutionMessage")
|
||||||
|
.HasMaxLength(1000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("LastExecutionRecordCount")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("LastExecutionStatus")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastExecutionTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("NextExecutionTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("ProfileId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ScheduleType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ScheduledDateTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SourceDatabaseOverride")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ProfileId");
|
||||||
|
|
||||||
|
b.ToTable("ProfileSchedules");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CredentialManager.Models.ScheduleExecutionHistory", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AdditionalInfo")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationInfo")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationType")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("EndTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ErrorDetails")
|
||||||
|
.HasMaxLength(5000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Message")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("ProfileId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ProfileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("RecordsProcessed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("RecordsWithErrors")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ScheduleId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("SourceInfo")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SourceType")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("StartTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("TriggerType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace CredentialManager.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddExternalIdRelationships : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "ExternalIdRelationshipsJson",
|
||||||
|
table: "DataCouplerProfiles",
|
||||||
|
type: "TEXT",
|
||||||
|
maxLength: 4000,
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ExternalIdRelationshipsJson",
|
||||||
|
table: "DataCouplerProfiles");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+601
@@ -0,0 +1,601 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using CredentialManager.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace CredentialManager.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(CredentialDbContext))]
|
||||||
|
[Migration("20260216113009_AddDefaultValuesJsonToProfile")]
|
||||||
|
partial class AddDefaultValuesJsonToProfile
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "9.0.6");
|
||||||
|
|
||||||
|
modelBuilder.Entity("CredentialManager.Models.CredentialEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AdditionalParameters")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("CommandTimeout")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(30);
|
||||||
|
|
||||||
|
b.Property<string>("ConnectionString")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("CreatedBy")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DatabaseName")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DatabaseType")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptedApiKey")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptedAuthToken")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("EncryptedPassword")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Headers")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Host")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IgnoreSslErrors")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(true);
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("OdbcDsnName")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("OdbcMode")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("Port")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("RestServiceType")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("TimeoutSeconds")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(100);
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("DatabaseType");
|
||||||
|
|
||||||
|
b.HasIndex("IsActive");
|
||||||
|
|
||||||
|
b.HasIndex("Name")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("Type");
|
||||||
|
|
||||||
|
b.ToTable("Credentials", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CredentialManager.Models.DataCouplerProfile", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<string>("CreatedBy")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultValuesJson")
|
||||||
|
.HasMaxLength(4000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DeletionAction")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DeletionMarkField")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DeletionMarkValue")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("DestinationCredentialId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationEndpoint")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationSchema")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationTable")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ExternalIdRelationshipsJson")
|
||||||
|
.HasMaxLength(4000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("FieldMappingJson")
|
||||||
|
.HasMaxLength(4000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(true);
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastUsedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("SourceCredentialId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("SourceCustomQuery")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SourceDatabaseName")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SourceFilePath")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SourceKeyField")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SourceSchema")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SourceTable")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SourceType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("SyncDeletions")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("UseRecordAssociations")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedAt");
|
||||||
|
|
||||||
|
b.HasIndex("DestinationCredentialId");
|
||||||
|
|
||||||
|
b.HasIndex("DestinationType");
|
||||||
|
|
||||||
|
b.HasIndex("IsActive");
|
||||||
|
|
||||||
|
b.HasIndex("LastUsedAt");
|
||||||
|
|
||||||
|
b.HasIndex("Name")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("SourceCredentialId");
|
||||||
|
|
||||||
|
b.HasIndex("SourceType");
|
||||||
|
|
||||||
|
b.ToTable("DataCouplerProfiles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CredentialManager.Models.KeyAssociation", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AdditionalInfo")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Data_Hash")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("DeletionSynced")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletionSyncedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationEntity")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationKeyField")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(true);
|
||||||
|
|
||||||
|
b.Property<bool>("IsSourceDeleted")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("KeyValue")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastVerifiedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("MappedDestinationField")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("RestCredentialName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SourceKeyField")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SourcesInfo")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedAt");
|
||||||
|
|
||||||
|
b.HasIndex("DestinationEntity");
|
||||||
|
|
||||||
|
b.HasIndex("IsActive");
|
||||||
|
|
||||||
|
b.HasIndex("KeyValue")
|
||||||
|
.HasDatabaseName("IX_KeyAssociations_KeyValue");
|
||||||
|
|
||||||
|
b.HasIndex("LastVerifiedAt");
|
||||||
|
|
||||||
|
b.HasIndex("RestCredentialName");
|
||||||
|
|
||||||
|
b.HasIndex("KeyValue", "DestinationEntity", "RestCredentialName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("IX_KeyAssociations_Unique");
|
||||||
|
|
||||||
|
b.ToTable("KeyAssociations", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CredentialManager.Models.ProfileSchedule", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("CreatedBy")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DailyTime")
|
||||||
|
.HasMaxLength(10)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("DayOfMonth")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("DayOfWeek")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationDatabaseOverride")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("EnableDeletionSync")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ExecutionCount")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("IntervalUnit")
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("IntervalValue")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("LastExecutionMessage")
|
||||||
|
.HasMaxLength(1000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("LastExecutionRecordCount")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("LastExecutionStatus")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastExecutionTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("NextExecutionTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("ProfileId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ScheduleType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ScheduledDateTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SourceDatabaseOverride")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ProfileId");
|
||||||
|
|
||||||
|
b.ToTable("ProfileSchedules");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CredentialManager.Models.ScheduleExecutionHistory", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AdditionalInfo")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationInfo")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationType")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("EndTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ErrorDetails")
|
||||||
|
.HasMaxLength(5000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Message")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("ProfileId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ProfileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("RecordsProcessed")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("RecordsWithErrors")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ScheduleId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("SourceInfo")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SourceType")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("StartTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("TriggerType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace CredentialManager.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddDefaultValuesJsonToProfile : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "DefaultValuesJson",
|
||||||
|
table: "DataCouplerProfiles",
|
||||||
|
type: "TEXT",
|
||||||
|
maxLength: 4000,
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "DefaultValuesJson",
|
||||||
|
table: "DataCouplerProfiles");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ namespace CredentialManager.Migrations
|
|||||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.0");
|
modelBuilder.HasAnnotation("ProductVersion", "9.0.6");
|
||||||
|
|
||||||
modelBuilder.Entity("CredentialManager.Models.CredentialEntity", b =>
|
modelBuilder.Entity("CredentialManager.Models.CredentialEntity", b =>
|
||||||
{
|
{
|
||||||
@@ -146,6 +146,10 @@ namespace CredentialManager.Migrations
|
|||||||
.HasMaxLength(100)
|
.HasMaxLength(100)
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DefaultValuesJson")
|
||||||
|
.HasMaxLength(4000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("DeletionAction")
|
b.Property<string>("DeletionAction")
|
||||||
.HasMaxLength(20)
|
.HasMaxLength(20)
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
@@ -182,6 +186,10 @@ namespace CredentialManager.Migrations
|
|||||||
.HasMaxLength(20)
|
.HasMaxLength(20)
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ExternalIdRelationshipsJson")
|
||||||
|
.HasMaxLength(4000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("FieldMappingJson")
|
b.Property<string>("FieldMappingJson")
|
||||||
.HasMaxLength(4000)
|
.HasMaxLength(4000)
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|||||||
@@ -174,13 +174,51 @@ public static class ConnectionStringBuilder
|
|||||||
};
|
};
|
||||||
} private static string BuildSqlServerConnectionString(DatabaseCredential credential)
|
} private static string BuildSqlServerConnectionString(DatabaseCredential credential)
|
||||||
{
|
{
|
||||||
var builder = new List<string>
|
var builder = new List<string>();
|
||||||
|
|
||||||
|
// Gestione speciale per SQL Server locale e named instances
|
||||||
|
// Se l'host contiene '\' (instance name) o '(localdb)', non aggiungere la porta
|
||||||
|
bool hasInstanceName = credential.Host.Contains('\\') ||
|
||||||
|
credential.Host.StartsWith("(localdb)", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (hasInstanceName)
|
||||||
{
|
{
|
||||||
$"Server={credential.Host},{credential.Port}",
|
// Per named instances e LocalDB, non includere la porta
|
||||||
$"User Id={credential.Username}",
|
builder.Add($"Server={credential.Host}");
|
||||||
$"Password={credential.Password}",
|
}
|
||||||
$"Connection Timeout={credential.CommandTimeout}"
|
else
|
||||||
};
|
{
|
||||||
|
// Per connessioni TCP/IP standard, include host e porta
|
||||||
|
// Ma solo se la porta non è la default (1433) per localhost
|
||||||
|
if ((credential.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
credential.Host == "." ||
|
||||||
|
credential.Host == "127.0.0.1") && credential.Port == 1433)
|
||||||
|
{
|
||||||
|
// Per localhost con porta default, ometti la porta per usare Named Pipes
|
||||||
|
builder.Add($"Server={credential.Host}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Per altri casi, usa host,porta
|
||||||
|
builder.Add($"Server={credential.Host},{credential.Port}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se username è vuoto o è "Integrated", usa Windows Authentication
|
||||||
|
if (string.IsNullOrWhiteSpace(credential.Username) ||
|
||||||
|
credential.Username.Equals("Integrated", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
credential.Username.Equals("Windows", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
builder.Add("Integrated Security=True");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Usa SQL Server Authentication
|
||||||
|
builder.Add($"User Id={credential.Username}");
|
||||||
|
builder.Add($"Password={credential.Password}");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Add($"Connection Timeout={credential.CommandTimeout}");
|
||||||
|
|
||||||
// Aggiungi Database solo se specificato
|
// Aggiungi Database solo se specificato
|
||||||
if (!string.IsNullOrEmpty(credential.DatabaseName))
|
if (!string.IsNullOrEmpty(credential.DatabaseName))
|
||||||
|
|||||||
@@ -59,6 +59,15 @@ public class DataCouplerProfile
|
|||||||
// Mapping dei campi salvato come JSON
|
// Mapping dei campi salvato come JSON
|
||||||
[MaxLength(4000)]
|
[MaxLength(4000)]
|
||||||
public string? FieldMappingJson { get; set; }
|
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; }
|
||||||
|
|
||||||
// Configurazione chiave sorgente e associazioni
|
// Configurazione chiave sorgente e associazioni
|
||||||
[MaxLength(200)]
|
[MaxLength(200)]
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ public class DataCouplerProfileDto
|
|||||||
// Mapping dei campi
|
// Mapping dei campi
|
||||||
public List<FieldMappingDto>? FieldMappings { get; set; }
|
public List<FieldMappingDto>? FieldMappings { get; set; }
|
||||||
|
|
||||||
|
// Default values per campi destinazione (FieldName -> (Value, Type))
|
||||||
|
public Dictionary<string, (object? Value, string? Type)>? DefaultValues { get; set; }
|
||||||
|
|
||||||
|
// External ID Relationships per Salesforce
|
||||||
|
public List<ExternalIdRelationshipDto>? ExternalIdRelationships { get; set; }
|
||||||
|
|
||||||
// Configurazione chiave sorgente e associazioni
|
// Configurazione chiave sorgente e associazioni
|
||||||
public string? SourceKeyField { get; set; }
|
public string? SourceKeyField { get; set; }
|
||||||
public bool UseRecordAssociations { get; set; }
|
public bool UseRecordAssociations { get; set; }
|
||||||
@@ -47,10 +53,48 @@ public class FieldMappingDto
|
|||||||
public bool IsRequired { get; set; }
|
public bool IsRequired { get; set; }
|
||||||
public string? DefaultValue { get; set; }
|
public string? DefaultValue { get; set; }
|
||||||
public string? Transformation { get; set; }
|
public string? Transformation { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lista di relazioni External ID associate a questo campo (per Salesforce)
|
||||||
|
/// </summary>
|
||||||
|
public List<ExternalIdRelationshipDto>? ExternalIdRelationships { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// DTO per la visualizzazione di un profilo nella lista
|
/// DTO per External ID Relationship (Salesforce)
|
||||||
|
/// </summary>
|
||||||
|
public class ExternalIdRelationshipDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Nome della relazione (es. "Account__r")
|
||||||
|
/// </summary>
|
||||||
|
public string RelationshipName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Nome dell'oggetto correlato (es. "Account")
|
||||||
|
/// </summary>
|
||||||
|
public string RelatedObjectName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Campo External ID dell'oggetto correlato (es. "Country__c")
|
||||||
|
/// </summary>
|
||||||
|
public string ExternalIdField { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Campo sorgente da cui prendere il valore per l'External ID
|
||||||
|
/// </summary>
|
||||||
|
public string SourceField { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>/// DTO per i valori di default
|
||||||
|
/// </summary>
|
||||||
|
public class DefaultValueDto
|
||||||
|
{
|
||||||
|
public object? Value { get; set; }
|
||||||
|
public string? Type { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>/// DTO per la visualizzazione di un profilo nella lista
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DataCouplerProfileSummaryDto
|
public class DataCouplerProfileSummaryDto
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CredentialManager.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Tipo di mapping field
|
||||||
|
/// </summary>
|
||||||
|
public enum MappingType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Mapping da campo sorgente a campo destinazione
|
||||||
|
/// </summary>
|
||||||
|
FieldMapping,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Valore di default per campo destinazione
|
||||||
|
/// </summary>
|
||||||
|
DefaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rappresenta una voce di mapping che può essere:
|
||||||
|
/// - Un mapping da campo sorgente a campo destinazione
|
||||||
|
/// - Un valore di default per un campo destinazione
|
||||||
|
/// </summary>
|
||||||
|
public class FieldMappingEntry
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Tipo di mapping
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public MappingType Type { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Nome del campo sorgente (solo per FieldMapping)
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("sourceField")]
|
||||||
|
public string? SourceField { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Nome del campo destinazione
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("destinationField")]
|
||||||
|
public string DestinationField { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Valore di default (solo per DefaultValue)
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("defaultValue")]
|
||||||
|
public object? DefaultValue { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tipo di dato del valore di default (per conversioni corrette)
|
||||||
|
/// Esempi: "string", "int", "decimal", "boolean", "datetime"
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("defaultValueType")]
|
||||||
|
public string? DefaultValueType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Crea un mapping da campo sorgente a campo destinazione
|
||||||
|
/// </summary>
|
||||||
|
public static FieldMappingEntry CreateFieldMapping(string sourceField, string destinationField)
|
||||||
|
{
|
||||||
|
return new FieldMappingEntry
|
||||||
|
{
|
||||||
|
Type = MappingType.FieldMapping,
|
||||||
|
SourceField = sourceField,
|
||||||
|
DestinationField = destinationField
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Crea un valore di default per un campo destinazione
|
||||||
|
/// </summary>
|
||||||
|
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)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determina automaticamente il tipo del valore
|
||||||
|
/// </summary>
|
||||||
|
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"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ottiene una descrizione user-friendly del mapping
|
||||||
|
/// </summary>
|
||||||
|
public string GetDescription()
|
||||||
|
{
|
||||||
|
return Type switch
|
||||||
|
{
|
||||||
|
MappingType.FieldMapping => $"{SourceField} → {DestinationField}",
|
||||||
|
MappingType.DefaultValue => $"{DestinationField} = {DefaultValue ?? "null"} ({DefaultValueType})",
|
||||||
|
_ => "Unknown"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper per la conversione tra vecchio formato (Dictionary) e nuovo formato (FieldMappingEntry)
|
||||||
|
/// </summary>
|
||||||
|
public static class MappingConverter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Converte il vecchio formato Dictionary in lista di FieldMappingEntry
|
||||||
|
/// </summary>
|
||||||
|
public static List<FieldMappingEntry> FromDictionary(Dictionary<string, string> oldMappings)
|
||||||
|
{
|
||||||
|
var entries = new List<FieldMappingEntry>();
|
||||||
|
|
||||||
|
foreach (var mapping in oldMappings)
|
||||||
|
{
|
||||||
|
entries.Add(FieldMappingEntry.CreateFieldMapping(mapping.Key, mapping.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converte una lista di FieldMappingEntry nel vecchio formato Dictionary (solo field mappings)
|
||||||
|
/// </summary>
|
||||||
|
public static Dictionary<string, string> ToDictionary(List<FieldMappingEntry> entries)
|
||||||
|
{
|
||||||
|
var dictionary = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
foreach (var entry in entries.Where(e => e.Type == MappingType.FieldMapping && !string.IsNullOrEmpty(e.SourceField)))
|
||||||
|
{
|
||||||
|
dictionary[entry.SourceField!] = entry.DestinationField;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dictionary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ottiene solo i valori di default da una lista di entries
|
||||||
|
/// </summary>
|
||||||
|
public static Dictionary<string, (object? Value, string? Type)> GetDefaultValues(List<FieldMappingEntry> entries)
|
||||||
|
{
|
||||||
|
var defaults = new Dictionary<string, (object?, string?)>();
|
||||||
|
|
||||||
|
foreach (var entry in entries.Where(e => e.Type == MappingType.DefaultValue))
|
||||||
|
{
|
||||||
|
defaults[entry.DestinationField] = (entry.DefaultValue, entry.DefaultValueType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaults;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ public class DataCouplerProfileService : IDataCouplerProfileService
|
|||||||
existingProfile.DestinationTable = profile.DestinationTable;
|
existingProfile.DestinationTable = profile.DestinationTable;
|
||||||
existingProfile.DestinationEndpoint = profile.DestinationEndpoint;
|
existingProfile.DestinationEndpoint = profile.DestinationEndpoint;
|
||||||
existingProfile.FieldMappingJson = profile.FieldMappingJson;
|
existingProfile.FieldMappingJson = profile.FieldMappingJson;
|
||||||
|
existingProfile.ExternalIdRelationshipsJson = profile.ExternalIdRelationshipsJson;
|
||||||
existingProfile.SourceKeyField = profile.SourceKeyField;
|
existingProfile.SourceKeyField = profile.SourceKeyField;
|
||||||
existingProfile.UseRecordAssociations = profile.UseRecordAssociations;
|
existingProfile.UseRecordAssociations = profile.UseRecordAssociations;
|
||||||
existingProfile.IsActive = profile.IsActive;
|
existingProfile.IsActive = profile.IsActive;
|
||||||
@@ -200,6 +201,100 @@ public class DataCouplerProfileService : IDataCouplerProfileService
|
|||||||
return new List<FieldMappingDto>();
|
return new List<FieldMappingDto>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serializza la lista di External ID Relationships in JSON
|
||||||
|
/// </summary>
|
||||||
|
public string SerializeExternalIdRelationships(List<ExternalIdRelationshipDto>? relationships)
|
||||||
|
{
|
||||||
|
if (relationships == null || !relationships.Any())
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
return JsonSerializer.Serialize(relationships, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <summary>
|
||||||
|
/// Deserializza il JSON delle External ID Relationships
|
||||||
|
/// </summary>
|
||||||
|
public List<ExternalIdRelationshipDto> DeserializeExternalIdRelationships(string? json)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
return new List<ExternalIdRelationshipDto>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<List<ExternalIdRelationshipDto>>(json, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
|
}) ?? new List<ExternalIdRelationshipDto>();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return new List<ExternalIdRelationshipDto>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serializza i default values in JSON
|
||||||
|
/// </summary>
|
||||||
|
public string SerializeDefaultValues(Dictionary<string, (object? Value, string? Type)>? defaultValues)
|
||||||
|
{
|
||||||
|
if (defaultValues == null || !defaultValues.Any())
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
// Converti in un formato serializzabile (Dictionary<string, DefaultValueDto>)
|
||||||
|
var serializable = new Dictionary<string, DefaultValueDto>();
|
||||||
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserializza il JSON dei default values
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, (object? Value, string? Type)> DeserializeDefaultValues(string? json)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
return new Dictionary<string, (object?, string?)>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var deserialized = JsonSerializer.Deserialize<Dictionary<string, DefaultValueDto>>(json, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
|
});
|
||||||
|
|
||||||
|
if (deserialized == null)
|
||||||
|
return new Dictionary<string, (object?, string?)>();
|
||||||
|
|
||||||
|
// Converti nel formato tuple
|
||||||
|
var result = new Dictionary<string, (object?, string?)>();
|
||||||
|
foreach (var entry in deserialized)
|
||||||
|
{
|
||||||
|
result[entry.Key] = (entry.Value.Value, entry.Value.Type);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return new Dictionary<string, (object?, string?)>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converte un DataCouplerProfile in DTO
|
/// Converte un DataCouplerProfile in DTO
|
||||||
@@ -226,6 +321,8 @@ public class DataCouplerProfileService : IDataCouplerProfileService
|
|||||||
DestinationTable = profile.DestinationTable,
|
DestinationTable = profile.DestinationTable,
|
||||||
DestinationEndpoint = profile.DestinationEndpoint,
|
DestinationEndpoint = profile.DestinationEndpoint,
|
||||||
FieldMappings = DeserializeFieldMappings(profile.FieldMappingJson),
|
FieldMappings = DeserializeFieldMappings(profile.FieldMappingJson),
|
||||||
|
DefaultValues = DeserializeDefaultValues(profile.DefaultValuesJson),
|
||||||
|
ExternalIdRelationships = DeserializeExternalIdRelationships(profile.ExternalIdRelationshipsJson),
|
||||||
SourceKeyField = profile.SourceKeyField,
|
SourceKeyField = profile.SourceKeyField,
|
||||||
UseRecordAssociations = profile.UseRecordAssociations
|
UseRecordAssociations = profile.UseRecordAssociations
|
||||||
};
|
};
|
||||||
@@ -254,6 +351,8 @@ public class DataCouplerProfileService : IDataCouplerProfileService
|
|||||||
DestinationTable = dto.DestinationTable,
|
DestinationTable = dto.DestinationTable,
|
||||||
DestinationEndpoint = dto.DestinationEndpoint,
|
DestinationEndpoint = dto.DestinationEndpoint,
|
||||||
FieldMappingJson = SerializeFieldMappings(dto.FieldMappings),
|
FieldMappingJson = SerializeFieldMappings(dto.FieldMappings),
|
||||||
|
DefaultValuesJson = SerializeDefaultValues(dto.DefaultValues),
|
||||||
|
ExternalIdRelationshipsJson = SerializeExternalIdRelationships(dto.ExternalIdRelationships),
|
||||||
SourceKeyField = dto.SourceKeyField,
|
SourceKeyField = dto.SourceKeyField,
|
||||||
UseRecordAssociations = dto.UseRecordAssociations,
|
UseRecordAssociations = dto.UseRecordAssociations,
|
||||||
CreatedBy = createdBy
|
CreatedBy = createdBy
|
||||||
|
|||||||
@@ -7,6 +7,13 @@
|
|||||||
<!-- Version is now automatically calculated by MinVer from git tags -->
|
<!-- Version is now automatically calculated by MinVer from git tags -->
|
||||||
<MinVerTagPrefix>v</MinVerTagPrefix>
|
<MinVerTagPrefix>v</MinVerTagPrefix>
|
||||||
<MinVerVerbosity>detailed</MinVerVerbosity>
|
<MinVerVerbosity>detailed</MinVerVerbosity>
|
||||||
|
|
||||||
|
<!-- Disabilita trimming per compatibilità Blazor Server -->
|
||||||
|
<PublishTrimmed>false</PublishTrimmed>
|
||||||
|
<!-- Abilita PublishSingleFile per deployment semplificato -->
|
||||||
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
|
<!-- Abilita ReadyToRun per migliori performance di avvio -->
|
||||||
|
<PublishReadyToRun>true</PublishReadyToRun>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -146,6 +146,19 @@ public partial class DataCoupler : ComponentBase
|
|||||||
isRestConnected = true;
|
isRestConnected = true;
|
||||||
|
|
||||||
Logger.LogInformation("Discovery batch completato: trovate {EntityCount} entità REST", restEntities.Count);
|
Logger.LogInformation("Discovery batch completato: trovate {EntityCount} entità REST", restEntities.Count);
|
||||||
|
|
||||||
|
// Carica anche i dettagli completi delle entità per External ID Relationships
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Logger.LogInformation("Caricamento dettagli entità per External ID Relationships...");
|
||||||
|
availableRelationshipObjects = await currentRestDiscovery.DiscoverEntitiesAsync();
|
||||||
|
Logger.LogInformation("Caricati {Count} oggetti disponibili per External ID Relationships", availableRelationshipObjects.Count);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Impossibile caricare i dettagli delle entità per External ID Relationships");
|
||||||
|
availableRelationshipObjects = new List<RestEntityInfo>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -474,12 +474,27 @@ else
|
|||||||
<label class="form-label">Host/Server *</label>
|
<label class="form-label">Host/Server *</label>
|
||||||
<InputText class="form-control" @bind-Value="currentDatabaseCredential.Host"
|
<InputText class="form-control" @bind-Value="currentDatabaseCredential.Host"
|
||||||
placeholder="es. localhost o server.dominio.com" />
|
placeholder="es. localhost o server.dominio.com" />
|
||||||
|
@if (currentDatabaseCredential.DatabaseType == DatabaseType.SqlServer)
|
||||||
|
{
|
||||||
|
<div class="form-text">
|
||||||
|
<strong>SQL Server locale:</strong><br/>
|
||||||
|
• Named Instance: <code>localhost\SQLEXPRESS</code> o <code>.\SQLEXPRESS</code><br/>
|
||||||
|
• LocalDB: <code>(localdb)\MSSQLLocalDB</code><br/>
|
||||||
|
• Default: <code>localhost</code> o <code>.</code> (usa porta 1433)
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Porta *</label>
|
<label class="form-label">Porta *</label>
|
||||||
<InputNumber class="form-control" @bind-Value="currentDatabaseCredential.Port" />
|
<InputNumber class="form-control" @bind-Value="currentDatabaseCredential.Port" />
|
||||||
|
@if (currentDatabaseCredential.DatabaseType == DatabaseType.SqlServer)
|
||||||
|
{
|
||||||
|
<div class="form-text">
|
||||||
|
<small>Ignorata per named instances e LocalDB</small>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -495,13 +510,26 @@ else
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Username *</label>
|
<label class="form-label">Username *</label>
|
||||||
<InputText class="form-control" @bind-Value="currentDatabaseCredential.Username" />
|
<InputText class="form-control" @bind-Value="currentDatabaseCredential.Username"
|
||||||
|
placeholder="o scrivi 'Integrated' per Windows Auth" />
|
||||||
|
@if (currentDatabaseCredential.DatabaseType == DatabaseType.SqlServer)
|
||||||
|
{
|
||||||
|
<div class="form-text">
|
||||||
|
<small>Per Windows Authentication, scrivi <strong>Integrated</strong> o lascia vuoto</small>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Password *</label>
|
<label class="form-label">Password *</label>
|
||||||
<InputText type="password" class="form-control" @bind-Value="currentDatabaseCredential.Password" />
|
<InputText type="password" class="form-control" @bind-Value="currentDatabaseCredential.Password" />
|
||||||
|
@if (currentDatabaseCredential.DatabaseType == DatabaseType.SqlServer)
|
||||||
|
{
|
||||||
|
<div class="form-text">
|
||||||
|
<small>Non richiesta per Windows Authentication</small>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -994,13 +1022,28 @@ else
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Altri database: validazione standard (Host, Username, Password)
|
// Altri database: validazione standard (Host, Username, Password)
|
||||||
if (string.IsNullOrEmpty(currentDatabaseCredential.Host) ||
|
// Per SQL Server, permetti Windows Authentication (username vuoto o "Integrated")
|
||||||
string.IsNullOrEmpty(currentDatabaseCredential.Username) ||
|
bool isSqlServerWithWindowsAuth = currentDatabaseCredential.DatabaseType == DatabaseType.SqlServer &&
|
||||||
string.IsNullOrEmpty(currentDatabaseCredential.Password))
|
(string.IsNullOrWhiteSpace(currentDatabaseCredential.Username) ||
|
||||||
|
currentDatabaseCredential.Username.Equals("Integrated", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
currentDatabaseCredential.Username.Equals("Windows", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(currentDatabaseCredential.Host))
|
||||||
{
|
{
|
||||||
await JSRuntime.InvokeVoidAsync("alert", "Compila tutti i campi obbligatori (Host, Username, Password).");
|
await JSRuntime.InvokeVoidAsync("alert", "Il campo Host è obbligatorio.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isSqlServerWithWindowsAuth)
|
||||||
|
{
|
||||||
|
// Per database che non usano Windows Authentication, richiedi username e password
|
||||||
|
if (string.IsNullOrEmpty(currentDatabaseCredential.Username) ||
|
||||||
|
string.IsNullOrEmpty(currentDatabaseCredential.Password))
|
||||||
|
{
|
||||||
|
await JSRuntime.InvokeVoidAsync("alert", "Username e Password sono obbligatori. Per SQL Server con Windows Authentication, inserisci 'Integrated' come username.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var (success, message) = await CredentialService.TestDatabaseConnectionAsync(currentDatabaseCredential);
|
var (success, message) = await CredentialService.TestDatabaseConnectionAsync(currentDatabaseCredential);
|
||||||
|
|||||||
@@ -920,23 +920,80 @@
|
|||||||
<!-- Colonna Centrale: Controlli Mapping -->
|
<!-- Colonna Centrale: Controlli Mapping -->
|
||||||
<div class="col-2 text-center">
|
<div class="col-2 text-center">
|
||||||
<div class="d-flex flex-column justify-content-center h-100">
|
<div class="d-flex flex-column justify-content-center h-100">
|
||||||
<button class="btn btn-success mb-2" @onclick="CreateMapping"
|
<!-- Toggle tra Mapping e Default Value -->
|
||||||
disabled="@(string.IsNullOrEmpty(selectedDbColumn) || string.IsNullOrEmpty(selectedRestProperty))">
|
<div class="btn-group mb-3" role="group">
|
||||||
<i class="fas fa-arrow-right"></i>
|
<button type="button"
|
||||||
<small class="d-block">Map</small>
|
class="btn btn-sm @(isAddingDefaultValue ? "btn-outline-primary" : "btn-primary")"
|
||||||
</button>
|
@onclick="@(() => isAddingDefaultValue = false)">
|
||||||
<button class="btn btn-danger mb-2" @onclick="RemoveMapping"
|
<i class="fas fa-arrows-alt-h"></i>
|
||||||
disabled="@(string.IsNullOrEmpty(selectedDbColumn) || !fieldMappings.ContainsKey(selectedDbColumn))">
|
<small class="d-block">Mapping</small>
|
||||||
<i class="fas fa-times"></i>
|
</button>
|
||||||
<small class="d-block">Remove</small>
|
<button type="button"
|
||||||
</button>
|
class="btn btn-sm @(isAddingDefaultValue ? "btn-warning" : "btn-outline-warning")"
|
||||||
<button class="btn btn-warning mb-2" @onclick="AutoMapFields">
|
@onclick="@(() => isAddingDefaultValue = true)">
|
||||||
<i class="fas fa-magic"></i>
|
<i class="fas fa-file-alt"></i>
|
||||||
<small class="d-block">Auto</small>
|
<small class="d-block">Default</small>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Controlli per Mapping Normale -->
|
||||||
|
@if (!isAddingDefaultValue)
|
||||||
|
{
|
||||||
|
<button class="btn btn-success mb-2" @onclick="CreateMapping"
|
||||||
|
disabled="@(string.IsNullOrEmpty(selectedDbColumn) || string.IsNullOrEmpty(selectedRestProperty))">
|
||||||
|
<i class="fas fa-arrow-right"></i>
|
||||||
|
<small class="d-block">Map</small>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger mb-2" @onclick="RemoveMapping"
|
||||||
|
disabled="@(string.IsNullOrEmpty(selectedDbColumn) || !fieldMappings.ContainsKey(selectedDbColumn))">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
<small class="d-block">Remove</small>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-warning mb-2" @onclick="AutoMapFields">
|
||||||
|
<i class="fas fa-magic"></i>
|
||||||
|
<small class="d-block">Auto</small>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<!-- Controlli per Default Value -->
|
||||||
|
<div class="mb-2">
|
||||||
|
<small class="text-muted d-block mb-1">Tipo Valore:</small>
|
||||||
|
<select class="form-select form-select-sm mb-2" @bind="defaultValueType">
|
||||||
|
<option value="string">String</option>
|
||||||
|
<option value="int">Integer</option>
|
||||||
|
<option value="decimal">Decimal</option>
|
||||||
|
<option value="boolean">Boolean</option>
|
||||||
|
<option value="datetime">DateTime</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" class="form-control form-control-sm mb-2"
|
||||||
|
placeholder="Valore default..."
|
||||||
|
@bind="defaultValueInput" />
|
||||||
|
<small class="text-muted d-block mb-2">
|
||||||
|
@if (defaultValueType == "datetime")
|
||||||
|
{
|
||||||
|
<span>Es: @DateTime.Now.ToString("yyyy-MM-dd")</span>
|
||||||
|
}
|
||||||
|
else if (defaultValueType == "boolean")
|
||||||
|
{
|
||||||
|
<span>Es: true o false</span>
|
||||||
|
}
|
||||||
|
else if (defaultValueType == "decimal")
|
||||||
|
{
|
||||||
|
<span>Es: 100.50</span>
|
||||||
|
}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-warning mb-2" @onclick="CreateDefaultValue"
|
||||||
|
disabled="@(string.IsNullOrEmpty(selectedRestProperty) || string.IsNullOrEmpty(defaultValueInput))">
|
||||||
|
<i class="fas fa-check"></i>
|
||||||
|
<small class="d-block">Set Default</small>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
<button class="btn btn-secondary" @onclick="ClearAllMappings">
|
<button class="btn btn-secondary" @onclick="ClearAllMappings">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
<small class="d-block">Clear</small>
|
<small class="d-block">Clear All</small>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -965,6 +1022,10 @@
|
|||||||
{
|
{
|
||||||
<span class="badge bg-success">Mapped</span>
|
<span class="badge bg-success">Mapped</span>
|
||||||
}
|
}
|
||||||
|
@if (defaultValues.ContainsKey(property.Name))
|
||||||
|
{
|
||||||
|
<span class="badge bg-warning text-dark">Default</span>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@@ -974,11 +1035,124 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sezione Mappature Correnti --> @if (fieldMappings.Any())
|
<!-- Sezione External ID Relationships (Salesforce) -->
|
||||||
|
@if (selectedRestEntity != null && currentRestDiscovery != null && IsSalesforceClient())
|
||||||
|
{
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="fas fa-link"></i> External ID Relationships (Salesforce)
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
<strong>Relating Records by External ID</strong><br>
|
||||||
|
<small>
|
||||||
|
Crea relazioni tra oggetti usando ID esterni invece degli ID interni di Salesforce.<br>
|
||||||
|
Esempio: Collega Opportunity ad Account usando <code>Account.CardCode__c = "C60000"</code>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form per aggiungere nuova relazione -->
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Oggetto Correlato:</label>
|
||||||
|
<select class="form-select" @bind="selectedRelationshipObject" @bind:after="OnRelationshipObjectSelected">
|
||||||
|
<option value="">-- Seleziona Oggetto --</option>
|
||||||
|
@foreach (var entity in availableRelationshipObjects.OrderBy(e => e.Name))
|
||||||
|
{
|
||||||
|
<option value="@entity.Name">@entity.Name</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
<small class="text-muted">Es: Account, Contact</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">External ID Field:</label>
|
||||||
|
<select class="form-select" @bind="selectedExternalIdField" disabled="@string.IsNullOrEmpty(selectedRelationshipObject)">
|
||||||
|
<option value="">-- Seleziona Campo --</option>
|
||||||
|
@foreach (var field in GetExternalIdFieldsForSelectedObject())
|
||||||
|
{
|
||||||
|
<option value="@field">@field</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
<small class="text-muted">Es: Country__c, CardCode__c</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Campo Sorgente:</label>
|
||||||
|
<select class="form-select" @bind="selectedRelationshipSourceField">
|
||||||
|
<option value="">-- Seleziona Campo --</option>
|
||||||
|
@foreach (var field in GetSourceFieldsForRelationship())
|
||||||
|
{
|
||||||
|
<option value="@field">@field</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
<small class="text-muted">Valore da usare per la relazione</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3 d-flex align-items-end">
|
||||||
|
<button class="btn btn-primary w-100" @onclick="AddExternalIdRelationship"
|
||||||
|
disabled="@(string.IsNullOrEmpty(selectedRelationshipObject) || string.IsNullOrEmpty(selectedExternalIdField) || string.IsNullOrEmpty(selectedRelationshipSourceField))">
|
||||||
|
<i class="fas fa-plus"></i> Aggiungi Relazione
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tabella relazioni configurate -->
|
||||||
|
@if (externalIdRelationships.Any())
|
||||||
|
{
|
||||||
|
<div class="mt-3">
|
||||||
|
<h6>Relazioni Configurate (@externalIdRelationships.Count)</h6>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Oggetto Correlato</th>
|
||||||
|
<th>External ID Field</th>
|
||||||
|
<th>Campo Sorgente</th>
|
||||||
|
<th>Formato JSON Output</th>
|
||||||
|
<th>Azioni</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var rel in externalIdRelationships)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td><strong>@rel.RelatedObjectName</strong></td>
|
||||||
|
<td><code>@rel.ExternalIdField</code></td>
|
||||||
|
<td><span class="badge bg-info">@rel.SourceField</span></td>
|
||||||
|
<td><small class="text-muted">@($"\"{rel.RelationshipName}\": {{ \"{rel.ExternalIdField}\": \"value\" }}")</small></td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-danger" @onclick="@(() => RemoveExternalIdRelationship(rel))">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="alert alert-secondary">
|
||||||
|
<i class="fas fa-info-circle"></i> Nessuna relazione External ID configurata. Aggiungine una se necessario.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Sezione Mappature Correnti --> @if (fieldMappings.Any() || defaultValues.Any())
|
||||||
{
|
{
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<h6>Mappature Correnti (@fieldMappings.Count)</h6>
|
<h6>Configurazione Mapping (@(fieldMappings.Count + defaultValues.Count) totali)</h6>
|
||||||
@if (keyFields.Any())
|
@if (keyFields.Any())
|
||||||
{
|
{
|
||||||
<small class="text-info">
|
<small class="text-info">
|
||||||
@@ -986,44 +1160,101 @@
|
|||||||
</small>
|
</small>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-sm table-striped">
|
<!-- Tabella Mapping Campi -->
|
||||||
<thead>
|
@if (fieldMappings.Any())
|
||||||
<tr>
|
{
|
||||||
<th>Campo Database</th>
|
<div class="card mb-3">
|
||||||
<th>Tipo DB</th>
|
<div class="card-header bg-light">
|
||||||
<th>→</th>
|
<i class="fas fa-arrows-alt-h"></i> <strong>Field Mappings</strong> (@fieldMappings.Count)
|
||||||
<th>Proprietà REST</th>
|
</div>
|
||||||
<th>Tipo REST</th>
|
<div class="card-body p-0">
|
||||||
<th>Azioni</th>
|
<div class="table-responsive">
|
||||||
</tr>
|
<table class="table table-sm table-striped mb-0">
|
||||||
</thead>
|
<thead>
|
||||||
<tbody>
|
<tr>
|
||||||
@foreach (var mapping in fieldMappings)
|
<th>Campo Sorgente</th>
|
||||||
{
|
<th>Tipo Sorgente</th>
|
||||||
DbColumnInfo? dbColumn = null;
|
<th>→</th>
|
||||||
if (selectedSourceType == "database" && !string.IsNullOrEmpty(selectedTable))
|
<th>Campo Destinazione</th>
|
||||||
{
|
<th>Tipo Destinazione</th>
|
||||||
dbColumn = databaseTables.ContainsKey(selectedTable) ?
|
<th>Azioni</th>
|
||||||
databaseTables[selectedTable].FirstOrDefault(c => c.Name == mapping.Key) : null;
|
</tr>
|
||||||
}
|
</thead>
|
||||||
var restProperty = restEntityDetails?.Properties.FirstOrDefault(p => p.Name == mapping.Value);
|
<tbody>
|
||||||
<tr>
|
@foreach (var mapping in fieldMappings)
|
||||||
<td><strong>@mapping.Key</strong></td>
|
{
|
||||||
<td><small class="text-muted">@(dbColumn?.DataType ?? (selectedSourceType == "file" ? "Text" : "Unknown"))</small></td>
|
DbColumnInfo? dbColumn = null;
|
||||||
<td><i class="fas fa-arrow-right text-success"></i></td>
|
if (selectedSourceType == "database" && !string.IsNullOrEmpty(selectedTable))
|
||||||
<td><strong>@mapping.Value</strong></td>
|
{
|
||||||
<td><small class="text-muted">@(restProperty?.Type ?? "Unknown")</small></td>
|
dbColumn = databaseTables.ContainsKey(selectedTable) ?
|
||||||
<td>
|
databaseTables[selectedTable].FirstOrDefault(c => c.Name == mapping.Key) : null;
|
||||||
<button class="btn btn-sm btn-danger" @onclick="@(() => RemoveSpecificMapping(mapping.Key))">
|
}
|
||||||
<i class="fas fa-trash"></i>
|
var restProperty = restEntityDetails?.Properties.FirstOrDefault(p => p.Name == mapping.Value);
|
||||||
</button>
|
<tr>
|
||||||
</td>
|
<td><strong>@mapping.Key</strong></td>
|
||||||
</tr>
|
<td><small class="text-muted">@(dbColumn?.DataType ?? (selectedSourceType == "file" ? "Text" : "Unknown"))</small></td>
|
||||||
}
|
<td><i class="fas fa-arrow-right text-success"></i></td>
|
||||||
</tbody>
|
<td><strong>@mapping.Value</strong></td>
|
||||||
</table>
|
<td><small class="text-muted">@(restProperty?.Type ?? "Unknown")</small></td>
|
||||||
</div>
|
<td>
|
||||||
|
<button class="btn btn-sm btn-danger" @onclick="@(() => RemoveSpecificMapping(mapping.Key))">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Tabella Default Values -->
|
||||||
|
@if (defaultValues.Any())
|
||||||
|
{
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header bg-warning text-dark">
|
||||||
|
<i class="fas fa-file-alt"></i> <strong>Default Values</strong> (@defaultValues.Count)
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-striped mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Campo Destinazione</th>
|
||||||
|
<th>Valore Default</th>
|
||||||
|
<th>Tipo Valore</th>
|
||||||
|
<th>Tipo Campo REST</th>
|
||||||
|
<th>Azioni</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var defaultValue in defaultValues)
|
||||||
|
{
|
||||||
|
var restProperty = restEntityDetails?.Properties.FirstOrDefault(p => p.Name == defaultValue.Key);
|
||||||
|
var (value, valueType) = defaultValue.Value;
|
||||||
|
<tr>
|
||||||
|
<td><strong>@defaultValue.Key</strong></td>
|
||||||
|
<td><code>@(value?.ToString() ?? "null")</code></td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-info">@valueType</span>
|
||||||
|
</td>
|
||||||
|
<td><small class="text-muted">@(restProperty?.Type ?? "Unknown")</small></td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-danger" @onclick="@(() => RemoveDefaultValue(defaultValue.Key))">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1153,6 +1384,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
@@ -1198,7 +1431,9 @@
|
|||||||
DestinationCredentialId="@(GetCurrentDestinationCredentialIdAsync().Result)"
|
DestinationCredentialId="@(GetCurrentDestinationCredentialIdAsync().Result)"
|
||||||
DestinationCredentialName="@selectedRestCredential"
|
DestinationCredentialName="@selectedRestCredential"
|
||||||
DestinationEndpoint="@selectedRestEntity?.Name"
|
DestinationEndpoint="@selectedRestEntity?.Name"
|
||||||
FieldMappings="@GetCurrentFieldMappings()"
|
FieldMappings="@GetCurrentFieldMappings()"
|
||||||
|
DefaultValues="@defaultValues"
|
||||||
|
ExternalIdRelationships="@externalIdRelationships"
|
||||||
SourceKeyField="@sourceKeyField"
|
SourceKeyField="@sourceKeyField"
|
||||||
UseRecordAssociations="@useRecordAssociations"
|
UseRecordAssociations="@useRecordAssociations"
|
||||||
OnProfileSaved="@OnProfileSaved" />
|
OnProfileSaved="@OnProfileSaved" />
|
||||||
|
|||||||
@@ -51,9 +51,24 @@ public partial class DataCoupler : ComponentBase
|
|||||||
(int)Math.Ceiling((double)fileData[sheetName].Count / pageSize) : 0;
|
(int)Math.Ceiling((double)fileData[sheetName].Count / pageSize) : 0;
|
||||||
|
|
||||||
// Mapping campi
|
// Mapping campi
|
||||||
private Dictionary<string, string> fieldMappings = new(); // DbColumn -> RestProperty
|
private Dictionary<string, string> fieldMappings = new(); // DbColumn -> RestProperty (legacy)
|
||||||
|
private List<FieldMappingEntry> fieldMappingEntries = new(); // New system: supporta sia mapping che default values
|
||||||
|
private Dictionary<string, (object? Value, string? Type)> defaultValues = new(); // DestinationField -> (DefaultValue, Type)
|
||||||
private HashSet<string> keyFields = new(); // REST properties marked as keys
|
private HashSet<string> keyFields = new(); // REST properties marked as keys
|
||||||
private string selectedDbColumn = "";
|
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<ExternalIdRelationshipDto> externalIdRelationships = new();
|
||||||
|
private string selectedRelationshipObject = "";
|
||||||
|
private string selectedExternalIdField = "";
|
||||||
|
private string selectedRelationshipSourceField = "";
|
||||||
|
private List<RestEntityInfo> availableRelationshipObjects = new(); // Oggetti disponibili per relazioni
|
||||||
|
|
||||||
// Gestione chiavi sorgente e associazioni
|
// Gestione chiavi sorgente e associazioni
|
||||||
private string sourceKeyField = ""; // Campo che identifica univocamente il record sorgente
|
private string sourceKeyField = ""; // Campo che identifica univocamente il record sorgente
|
||||||
@@ -338,11 +353,13 @@ public partial class DataCoupler : ComponentBase
|
|||||||
|
|
||||||
// Applica i mapping
|
// Applica i mapping
|
||||||
fieldMappings.Clear();
|
fieldMappings.Clear();
|
||||||
|
fieldMappingEntries.Clear();
|
||||||
keyFields.Clear();
|
keyFields.Clear();
|
||||||
|
|
||||||
foreach (var mapping in mappings)
|
foreach (var mapping in mappings)
|
||||||
{
|
{
|
||||||
fieldMappings[mapping.SourceField] = mapping.DestinationField;
|
fieldMappings[mapping.SourceField] = mapping.DestinationField;
|
||||||
|
fieldMappingEntries.Add(FieldMappingEntry.CreateFieldMapping(mapping.SourceField, mapping.DestinationField));
|
||||||
if (mapping.IsKey)
|
if (mapping.IsKey)
|
||||||
{
|
{
|
||||||
keyFields.Add(mapping.DestinationField);
|
keyFields.Add(mapping.DestinationField);
|
||||||
@@ -363,6 +380,42 @@ public partial class DataCoupler : ComponentBase
|
|||||||
{
|
{
|
||||||
Logger.LogInformation("Nessun mapping campi da applicare");
|
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<Dictionary<string, DefaultValueDto>>(
|
||||||
|
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
|
// Step 5: Applica configurazione chiave sorgente
|
||||||
if (!string.IsNullOrEmpty(profile.SourceKeyField))
|
if (!string.IsNullOrEmpty(profile.SourceKeyField))
|
||||||
@@ -374,6 +427,33 @@ public partial class DataCoupler : ComponentBase
|
|||||||
{
|
{
|
||||||
Logger.LogInformation("Nessuna chiave sorgente da applicare");
|
Logger.LogInformation("Nessuna chiave sorgente da applicare");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Step 5.5: Carica External ID Relationships (Salesforce)
|
||||||
|
if (!string.IsNullOrEmpty(profile.ExternalIdRelationshipsJson))
|
||||||
|
{
|
||||||
|
Logger.LogInformation("Step 5.5 - Caricamento External ID Relationships...");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var relationships = System.Text.Json.JsonSerializer.Deserialize<List<ExternalIdRelationshipDto>>(
|
||||||
|
profile.ExternalIdRelationshipsJson,
|
||||||
|
new System.Text.Json.JsonSerializerOptions { PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase });
|
||||||
|
|
||||||
|
if (relationships != null && relationships.Any())
|
||||||
|
{
|
||||||
|
externalIdRelationships.Clear();
|
||||||
|
externalIdRelationships.AddRange(relationships);
|
||||||
|
Logger.LogInformation("External ID Relationships caricate - Totale: {Count}", externalIdRelationships.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Errore nel caricamento delle External ID Relationships dal profilo");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogInformation("Nessuna External ID Relationship da applicare");
|
||||||
|
}
|
||||||
|
|
||||||
// Step 6: Applica configurazione associazioni record
|
// Step 6: Applica configurazione associazioni record
|
||||||
useRecordAssociations = profile.UseRecordAssociations;
|
useRecordAssociations = profile.UseRecordAssociations;
|
||||||
@@ -687,7 +767,10 @@ public partial class DataCoupler : ComponentBase
|
|||||||
ResetSourceState();
|
ResetSourceState();
|
||||||
ResetDestinationState();
|
ResetDestinationState();
|
||||||
fieldMappings.Clear();
|
fieldMappings.Clear();
|
||||||
|
fieldMappingEntries.Clear();
|
||||||
|
defaultValues.Clear();
|
||||||
keyFields.Clear();
|
keyFields.Clear();
|
||||||
|
externalIdRelationships.Clear(); // Reset relazioni
|
||||||
transferResults.Clear();
|
transferResults.Clear();
|
||||||
transferMessage = "";
|
transferMessage = "";
|
||||||
}
|
}
|
||||||
@@ -1293,6 +1376,17 @@ public partial class DataCoupler : ComponentBase
|
|||||||
// Crea il nuovo mapping
|
// Crea il nuovo mapping
|
||||||
fieldMappings[selectedDbColumn] = selectedRestProperty;
|
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);
|
Logger.LogInformation("Creato mapping: {DbColumn} -> {RestProperty}", selectedDbColumn, selectedRestProperty);
|
||||||
|
|
||||||
// Deseleziona i campi
|
// Deseleziona i campi
|
||||||
@@ -1300,14 +1394,108 @@ public partial class DataCoupler : ComponentBase
|
|||||||
selectedRestProperty = "";
|
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()
|
private void RemoveMapping()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(selectedDbColumn) || !fieldMappings.ContainsKey(selectedDbColumn))
|
if (string.IsNullOrEmpty(selectedDbColumn) || !fieldMappings.ContainsKey(selectedDbColumn))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
fieldMappings.Remove(selectedDbColumn);
|
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);
|
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)
|
private void RemoveSpecificMapping(string dbColumn)
|
||||||
{
|
{
|
||||||
if (fieldMappings.ContainsKey(dbColumn))
|
if (fieldMappings.ContainsKey(dbColumn))
|
||||||
@@ -1320,12 +1508,137 @@ public partial class DataCoupler : ComponentBase
|
|||||||
private void ClearAllMappings()
|
private void ClearAllMappings()
|
||||||
{
|
{
|
||||||
fieldMappings.Clear();
|
fieldMappings.Clear();
|
||||||
|
fieldMappingEntries.Clear();
|
||||||
|
defaultValues.Clear();
|
||||||
selectedDbColumn = "";
|
selectedDbColumn = "";
|
||||||
selectedRestProperty = "";
|
selectedRestProperty = "";
|
||||||
sourceKeyField = "";
|
sourceKeyField = "";
|
||||||
transferMessage = "";
|
transferMessage = "";
|
||||||
transferMessageType = "";
|
transferMessageType = "";
|
||||||
Logger.LogInformation("Tutti i mapping e le configurazioni sono stati cancellati");
|
isAddingDefaultValue = false;
|
||||||
|
defaultValueField = "";
|
||||||
|
defaultValueInput = "";
|
||||||
|
externalIdRelationships.Clear(); // Pulisce anche le relazioni
|
||||||
|
Logger.LogInformation("Tutti i mapping, default values e le configurazioni sono stati cancellati");
|
||||||
|
}
|
||||||
|
|
||||||
|
// External ID Relationships Methods
|
||||||
|
|
||||||
|
private void OnRelationshipObjectSelected()
|
||||||
|
{
|
||||||
|
// Il valore è già impostato tramite @bind, resettiamo solo i campi dipendenti
|
||||||
|
selectedExternalIdField = ""; // Reset campo External ID quando cambia l'oggetto
|
||||||
|
selectedRelationshipSourceField = ""; // Reset anche campo sorgente
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddExternalIdRelationship()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(selectedRelationshipObject) ||
|
||||||
|
string.IsNullOrEmpty(selectedExternalIdField) ||
|
||||||
|
string.IsNullOrEmpty(selectedRelationshipSourceField))
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Impossibile aggiungere relazione: campi mancanti");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trova il nome dell'oggetto correlato
|
||||||
|
var relatedObject = availableRelationshipObjects.FirstOrDefault(o => o.Name == selectedRelationshipObject);
|
||||||
|
if (relatedObject == null)
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Oggetto correlato non trovato: {ObjectName}", selectedRelationshipObject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determina il nome della relazione in base al tipo di oggetto
|
||||||
|
// Salesforce: oggetti STANDARD usano solo il nome (es. "Account")
|
||||||
|
// oggetti CUSTOM (finiscono con __c) usano __r (es. "CustomObject__r")
|
||||||
|
string relationshipName;
|
||||||
|
if (selectedRelationshipObject.EndsWith("__c"))
|
||||||
|
{
|
||||||
|
// Oggetto custom: rimuovi __c e aggiungi __r
|
||||||
|
relationshipName = selectedRelationshipObject.Replace("__c", "__r");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Oggetto standard: usa solo il nome
|
||||||
|
relationshipName = selectedRelationshipObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crea la relazione
|
||||||
|
var relationship = new ExternalIdRelationshipDto
|
||||||
|
{
|
||||||
|
RelationshipName = relationshipName,
|
||||||
|
RelatedObjectName = selectedRelationshipObject,
|
||||||
|
ExternalIdField = selectedExternalIdField,
|
||||||
|
SourceField = selectedRelationshipSourceField
|
||||||
|
};
|
||||||
|
|
||||||
|
// Verifica duplicati
|
||||||
|
if (externalIdRelationships.Any(r =>
|
||||||
|
r.RelatedObjectName == relationship.RelatedObjectName &&
|
||||||
|
r.ExternalIdField == relationship.ExternalIdField))
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Relazione già esistente per questo oggetto e campo External ID");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
externalIdRelationships.Add(relationship);
|
||||||
|
|
||||||
|
Logger.LogInformation("Aggiunta relazione External ID: {Relationship}.{Field} <- {SourceField}",
|
||||||
|
relationship.RelationshipName, relationship.ExternalIdField, relationship.SourceField);
|
||||||
|
|
||||||
|
// Reset campi
|
||||||
|
selectedRelationshipObject = "";
|
||||||
|
selectedExternalIdField = "";
|
||||||
|
selectedRelationshipSourceField = "";
|
||||||
|
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveExternalIdRelationship(ExternalIdRelationshipDto relationship)
|
||||||
|
{
|
||||||
|
if (externalIdRelationships.Remove(relationship))
|
||||||
|
{
|
||||||
|
Logger.LogInformation("Rimossa relazione External ID: {Relationship}.{Field}",
|
||||||
|
relationship.RelationshipName, relationship.ExternalIdField);
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<string> GetExternalIdFieldsForSelectedObject()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(selectedRelationshipObject))
|
||||||
|
return new List<string>();
|
||||||
|
|
||||||
|
var entity = availableRelationshipObjects.FirstOrDefault(e => e.Name == selectedRelationshipObject);
|
||||||
|
if (entity == null)
|
||||||
|
return new List<string>();
|
||||||
|
|
||||||
|
// Filtra i campi che potrebbero essere External ID (tipicamente campo con __c o specifici tipi)
|
||||||
|
return entity.Properties
|
||||||
|
.Where(p => p.Name.EndsWith("__c") || p.Name == "Id" || p.Name.Contains("External"))
|
||||||
|
.Select(p => p.Name)
|
||||||
|
.OrderBy(p => p)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<string> GetSourceFieldsForRelationship()
|
||||||
|
{
|
||||||
|
// Restituisce i campi sorgente disponibili
|
||||||
|
if (selectedSourceType == "database")
|
||||||
|
{
|
||||||
|
if (useCustomQuery && queryColumns.Any())
|
||||||
|
return queryColumns.ToList();
|
||||||
|
else if (!useCustomQuery && !string.IsNullOrEmpty(selectedTable) && databaseTables.ContainsKey(selectedTable))
|
||||||
|
return databaseTables[selectedTable].Select(c => c.Name).ToList();
|
||||||
|
}
|
||||||
|
else if (selectedSourceType == "file" && fileSheets.ContainsKey(selectedSheet))
|
||||||
|
{
|
||||||
|
return fileSheets[selectedSheet].ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new List<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AutoMapFields()
|
private void AutoMapFields()
|
||||||
@@ -1943,11 +2256,26 @@ public partial class DataCoupler : ComponentBase
|
|||||||
{
|
{
|
||||||
var restData = new Dictionary<string, object>();
|
var restData = new Dictionary<string, object>();
|
||||||
|
|
||||||
|
// Crea un set con i campi sorgente usati in External ID Relationships
|
||||||
|
// per escluderli dai mapping normali (verranno gestiti separatamente)
|
||||||
|
var externalIdSourceFields = externalIdRelationships
|
||||||
|
.Where(r => !string.IsNullOrWhiteSpace(r.SourceField))
|
||||||
|
.Select(r => r.SourceField)
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
|
// STEP 1: Applica i mapping normali (campo sorgente -> campo destinazione)
|
||||||
foreach (var mapping in fieldMappings)
|
foreach (var mapping in fieldMappings)
|
||||||
{
|
{
|
||||||
string dbColumn = mapping.Key;
|
string dbColumn = mapping.Key;
|
||||||
string restProperty = mapping.Value;
|
string restProperty = mapping.Value;
|
||||||
|
|
||||||
|
// Salta il mapping se il campo è usato in un External ID Relationship
|
||||||
|
if (externalIdSourceFields.Contains(dbColumn))
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Campo {DbColumn} usato in External ID Relationship, escluso da mapping normale", dbColumn);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (dbRecord.ContainsKey(dbColumn))
|
if (dbRecord.ContainsKey(dbColumn))
|
||||||
{
|
{
|
||||||
var value = dbRecord[dbColumn];
|
var value = dbRecord[dbColumn];
|
||||||
@@ -1962,9 +2290,61 @@ public partial class DataCoupler : ComponentBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.LogDebug("Record trasformato: {DbColumns} → {RestProperties}",
|
// 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)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(relationship.SourceField) &&
|
||||||
|
dbRecord.ContainsKey(relationship.SourceField))
|
||||||
|
{
|
||||||
|
var sourceValue = dbRecord[relationship.SourceField];
|
||||||
|
var transformedValue = TransformValue(sourceValue, relationship.SourceField, relationship.ExternalIdField);
|
||||||
|
|
||||||
|
if (transformedValue != null)
|
||||||
|
{
|
||||||
|
// Crea il dizionario annidato per l'External ID Relationship
|
||||||
|
// Formato: { "Account": { "CardCode__c": "V50000" } }
|
||||||
|
var externalIdObject = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ relationship.ExternalIdField, transformedValue }
|
||||||
|
};
|
||||||
|
|
||||||
|
restData[relationship.RelationshipName] = externalIdObject;
|
||||||
|
|
||||||
|
Logger.LogDebug("Aggiunta External ID Relationship: {RelationshipName}.{ExternalIdField} = {Value} (from {SourceField})",
|
||||||
|
relationship.RelationshipName, relationship.ExternalIdField, transformedValue, relationship.SourceField);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogDebug("Record trasformato: {DbColumns} → {RestProperties} (inclusi {DefaultCount} default values)",
|
||||||
string.Join(", ", dbRecord.Keys),
|
string.Join(", ", dbRecord.Keys),
|
||||||
string.Join(", ", restData.Keys));
|
string.Join(", ", restData.Keys),
|
||||||
|
defaultValues.Count(dv => restData.ContainsKey(dv.Key)));
|
||||||
|
|
||||||
return restData;
|
return restData;
|
||||||
}
|
}
|
||||||
@@ -2477,13 +2857,6 @@ public partial class DataCoupler : ComponentBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verifica che non contenga commenti SQL potenzialmente pericolosi
|
|
||||||
if (upperQuery.Contains("--") || upperQuery.Contains("/*"))
|
|
||||||
{
|
|
||||||
Logger.LogWarning("Query rifiutata: contiene commenti SQL non consentiti");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -164,18 +164,25 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
|
|||||||
throw new InvalidOperationException("Nessun mapping dei campi configurato per il profilo");
|
throw new InvalidOperationException("Nessun mapping dei campi configurato per il profilo");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4.5. Parse External ID Relationships (Salesforce)
|
||||||
|
var externalIdRelationships = ParseExternalIdRelationships(profile.ExternalIdRelationshipsJson);
|
||||||
|
if (externalIdRelationships.Any())
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Caricate {Count} External ID Relationships dal profilo", externalIdRelationships.Count);
|
||||||
|
}
|
||||||
|
|
||||||
// 5. Determina se utilizzare Salesforce Composite API
|
// 5. Determina se utilizzare Salesforce Composite API
|
||||||
bool useSalesforceComposite = restClient is DataConnection.REST.Implementations.SalesforceServiceClient;
|
bool useSalesforceComposite = restClient is DataConnection.REST.Implementations.SalesforceServiceClient;
|
||||||
|
|
||||||
if (useSalesforceComposite)
|
if (useSalesforceComposite)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Utilizzo Salesforce Composite API per il trasferimento");
|
_logger.LogInformation("Utilizzo Salesforce Composite API per il trasferimento");
|
||||||
return await ExecuteDataTransferWithCompositeAsync(profile, sourceRecords, restClient, restEntity, restCredential!, fieldMappings, enableDeletionSync);
|
return await ExecuteDataTransferWithCompositeAsync(profile, sourceRecords, restClient, restEntity, restCredential!, fieldMappings, externalIdRelationships, enableDeletionSync);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Utilizzo metodo trasferimento standard per il trasferimento");
|
_logger.LogInformation("Utilizzo metodo trasferimento standard per il trasferimento");
|
||||||
return await ExecuteDataTransferStandardAsync(profile, sourceRecords, restClient, restEntity, restCredential!, fieldMappings, enableDeletionSync);
|
return await ExecuteDataTransferStandardAsync(profile, sourceRecords, restClient, restEntity, restCredential!, fieldMappings, externalIdRelationships, enableDeletionSync);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -363,6 +370,53 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
|
|||||||
return mappings;
|
return mappings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserializza gli External ID Relationships dal JSON del profilo
|
||||||
|
/// </summary>
|
||||||
|
private List<ExternalIdRelationshipDto> ParseExternalIdRelationships(string? externalIdRelationshipsJson)
|
||||||
|
{
|
||||||
|
var relationships = new List<ExternalIdRelationshipDto>();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(externalIdRelationshipsJson))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("ExternalIdRelationships JSON è vuoto o null");
|
||||||
|
return relationships;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Parsing ExternalIdRelationships JSON: {Json}", externalIdRelationshipsJson);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var options = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
|
};
|
||||||
|
|
||||||
|
var relationshipsList = JsonSerializer.Deserialize<List<ExternalIdRelationshipDto>>(externalIdRelationshipsJson, options);
|
||||||
|
if (relationshipsList != null)
|
||||||
|
{
|
||||||
|
relationships = relationshipsList;
|
||||||
|
_logger.LogInformation("Trovati {Count} External ID Relationships nel JSON", relationships.Count);
|
||||||
|
|
||||||
|
foreach (var rel in relationships)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("External ID Relationship: {RelationshipName} - {RelatedObject}.{ExternalIdField} <- {SourceField}",
|
||||||
|
rel.RelationshipName, rel.RelatedObjectName, rel.ExternalIdField, rel.SourceField);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Deserializzazione ritornato null per ExternalIdRelationships JSON");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Errore nel parsing degli ExternalIdRelationships: {Json}", externalIdRelationshipsJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
return relationships;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ottiene tutti i record dal database
|
/// Ottiene tutti i record dal database
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -631,6 +685,7 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
|
|||||||
RestEntitySummary restEntity,
|
RestEntitySummary restEntity,
|
||||||
RestApiCredential restCredential,
|
RestApiCredential restCredential,
|
||||||
Dictionary<string, string> fieldMappings,
|
Dictionary<string, string> fieldMappings,
|
||||||
|
List<ExternalIdRelationshipDto> externalIdRelationships,
|
||||||
bool enableDeletionSync = false)
|
bool enableDeletionSync = false)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Iniziando trasferimento dati standard per {RecordCount} record - DeletionSync: {DeletionSync}",
|
_logger.LogInformation("Iniziando trasferimento dati standard per {RecordCount} record - DeletionSync: {DeletionSync}",
|
||||||
@@ -644,8 +699,8 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 1. Trasforma il record utilizzando i field mappings
|
// 1. Trasforma il record utilizzando i field mappings e External ID Relationships
|
||||||
var restData = TransformRecordForRest(record, fieldMappings);
|
var restData = TransformRecordForRest(record, fieldMappings, externalIdRelationships);
|
||||||
|
|
||||||
// 2. Gestione associazioni record se abilitata
|
// 2. Gestione associazioni record se abilitata
|
||||||
string? entityId = null;
|
string? entityId = null;
|
||||||
@@ -755,6 +810,7 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
|
|||||||
RestEntitySummary restEntity,
|
RestEntitySummary restEntity,
|
||||||
RestApiCredential restCredential,
|
RestApiCredential restCredential,
|
||||||
Dictionary<string, string> fieldMappings,
|
Dictionary<string, string> fieldMappings,
|
||||||
|
List<ExternalIdRelationshipDto> externalIdRelationships,
|
||||||
bool enableDeletionSync = false)
|
bool enableDeletionSync = false)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Iniziando trasferimento dati COMPOSITE per {RecordCount} record - DeletionSync: {DeletionSync}",
|
_logger.LogInformation("Iniziando trasferimento dati COMPOSITE per {RecordCount} record - DeletionSync: {DeletionSync}",
|
||||||
@@ -764,7 +820,7 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
|
|||||||
if (!(restClient is DataConnection.REST.Implementations.SalesforceServiceClient salesforceClient))
|
if (!(restClient is DataConnection.REST.Implementations.SalesforceServiceClient salesforceClient))
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Client REST non è SalesforceServiceClient, fallback al metodo standard");
|
_logger.LogWarning("Client REST non è SalesforceServiceClient, fallback al metodo standard");
|
||||||
return await ExecuteDataTransferStandardAsync(profile, sourceRecords, restClient, restEntity, restCredential, fieldMappings, enableDeletionSync);
|
return await ExecuteDataTransferStandardAsync(profile, sourceRecords, restClient, restEntity, restCredential, fieldMappings, externalIdRelationships, enableDeletionSync);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -794,8 +850,8 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
|
|||||||
var record = indexedRecord.Record;
|
var record = indexedRecord.Record;
|
||||||
var recordNumber = indexedRecord.RecordNumber;
|
var recordNumber = indexedRecord.RecordNumber;
|
||||||
|
|
||||||
// Trasforma il record in base ai mapping (operazione locale, thread-safe)
|
// Trasforma il record in base ai mapping e External ID Relationships (operazione locale, thread-safe)
|
||||||
var restData = TransformRecordForRest(record, fieldMappings);
|
var restData = TransformRecordForRest(record, fieldMappings, externalIdRelationships);
|
||||||
|
|
||||||
// Genera la chiave sorgente e l'hash dei dati per questo record (include MAPPING_SIGNATURE)
|
// Genera la chiave sorgente e l'hash dei dati per questo record (include MAPPING_SIGNATURE)
|
||||||
var sourceKey = GenerateSourceKey(record, profile.SourceKeyField);
|
var sourceKey = GenerateSourceKey(record, profile.SourceKeyField);
|
||||||
@@ -1085,7 +1141,10 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Trasforma un record sorgente in formato REST utilizzando i field mappings
|
/// Trasforma un record sorgente in formato REST utilizzando i field mappings
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private Dictionary<string, object> TransformRecordForRest(Dictionary<string, object> sourceRecord, Dictionary<string, string> fieldMappings)
|
private Dictionary<string, object> TransformRecordForRest(
|
||||||
|
Dictionary<string, object> sourceRecord,
|
||||||
|
Dictionary<string, string> fieldMappings,
|
||||||
|
List<ExternalIdRelationshipDto>? externalIdRelationships = null)
|
||||||
{
|
{
|
||||||
var restData = new Dictionary<string, object>();
|
var restData = new Dictionary<string, object>();
|
||||||
|
|
||||||
@@ -1105,6 +1164,35 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Aggiungi External ID Relationships (per Salesforce)
|
||||||
|
if (externalIdRelationships != null && externalIdRelationships.Any())
|
||||||
|
{
|
||||||
|
foreach (var relationship in externalIdRelationships)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(relationship.SourceField) &&
|
||||||
|
sourceRecord.ContainsKey(relationship.SourceField))
|
||||||
|
{
|
||||||
|
var sourceValue = sourceRecord[relationship.SourceField];
|
||||||
|
var transformedValue = TransformValueForRest(sourceValue);
|
||||||
|
|
||||||
|
if (transformedValue != null)
|
||||||
|
{
|
||||||
|
// Crea il dizionario annidato per l'External ID Relationship
|
||||||
|
// Formato: { "Account__r": { "Country__c": "US" } }
|
||||||
|
var externalIdObject = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ relationship.ExternalIdField, transformedValue }
|
||||||
|
};
|
||||||
|
|
||||||
|
restData[relationship.RelationshipName] = externalIdObject;
|
||||||
|
|
||||||
|
_logger.LogDebug("Aggiunta External ID Relationship: {RelationshipName} → {ExternalIdField} = {Value}",
|
||||||
|
relationship.RelationshipName, relationship.ExternalIdField, transformedValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return restData;
|
return restData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,350 @@
|
|||||||
|
# Implementazione External ID Relationships per Salesforce
|
||||||
|
|
||||||
|
## 📋 Panoramica
|
||||||
|
|
||||||
|
Implementata la funzionalità completa per gestire **External ID Relationships** nell'interfaccia di mapping dei campi di Data-Coupler. Questa feature permette di creare relazioni tra oggetti Salesforce utilizzando External ID durante il trasferimento dati, evitando la necessità di conoscere gli ID Salesforce interni.
|
||||||
|
|
||||||
|
## 🎯 Obiettivi Raggiunti
|
||||||
|
|
||||||
|
- ✅ Estensione modelli dati (DTO ed Entity) per supportare External ID Relationships
|
||||||
|
- ✅ UI completa per configurazione relazioni con autocomplete
|
||||||
|
- ✅ Logica di trasformazione dati integrata in DataCoupler e ScheduledProfileExecutionService
|
||||||
|
- ✅ Supporto per salvataggio e caricamento relazioni in profili Data Coupler
|
||||||
|
- ✅ Migrazione database per persistenza configurazioni
|
||||||
|
- ✅ Supporto per esecuzioni schedulate
|
||||||
|
|
||||||
|
## 🏗️ Architettura Implementata
|
||||||
|
|
||||||
|
### 1. Modelli Dati
|
||||||
|
|
||||||
|
#### **ExternalIdRelationshipDto** (CredentialManager/Models/DataCouplerProfileDto.cs)
|
||||||
|
```csharp
|
||||||
|
public class ExternalIdRelationshipDto
|
||||||
|
{
|
||||||
|
public string RelationshipName { get; set; } = string.Empty; // Es: "Account__r"
|
||||||
|
public string RelatedObjectName { get; set; } = string.Empty; // Es: "Account"
|
||||||
|
public string ExternalIdField { get; set; } = string.Empty; // Es: "Country__c"
|
||||||
|
public string SourceField { get; set; } = string.Empty; // Campo sorgente con valore
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **DataCouplerProfile Entity** (CredentialManager/Models/DataCouplerProfile.cs)
|
||||||
|
```csharp
|
||||||
|
[MaxLength(4000)]
|
||||||
|
public string? ExternalIdRelationshipsJson { get; set; }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Serializzazione/Deserializzazione
|
||||||
|
|
||||||
|
#### **DataCouplerProfileService** (CredentialManager/Services/DataCouplerProfileService.cs)
|
||||||
|
|
||||||
|
**Metodi Aggiunti:**
|
||||||
|
- `SerializeExternalIdRelationships()` - Serializza lista DTO → JSON
|
||||||
|
- `DeserializeExternalIdRelationships()` - Deserializza JSON → lista DTO
|
||||||
|
- Aggiornato `ToDto()` per includere External ID Relationships
|
||||||
|
- Aggiornato `FromDto()` per serializzare relazioni
|
||||||
|
- Aggiornato `UpdateProfileAsync()` per persistere ExternalIdRelationshipsJson
|
||||||
|
|
||||||
|
### 3. Interfaccia Utente
|
||||||
|
|
||||||
|
#### **DataCoupler.razor** - Sezione External ID Relationships
|
||||||
|
|
||||||
|
**Componenti UI:**
|
||||||
|
1. **Selezione Oggetto Correlato**: Dropdown con tutti gli oggetti REST disponibili
|
||||||
|
2. **Selezione External ID Field**: Dropdown con campi filtrati (terminanti con `__c`, `Id`, contengono "External")
|
||||||
|
3. **Selezione Campo Sorgente**: Dropdown con campi disponibili dalla sorgente dati
|
||||||
|
4. **Pulsante Aggiungi**: Conferma e aggiunge relazione alla lista
|
||||||
|
5. **Tabella Relazioni**: Visualizza tutte le relazioni configurate con formato di esempio
|
||||||
|
|
||||||
|
**Visibilità Condizionale:**
|
||||||
|
```csharp
|
||||||
|
@if (fieldMappings.Any() && currentRestDiscovery != null && IsSalesforceClient())
|
||||||
|
```
|
||||||
|
- Mostrata solo per connessioni Salesforce
|
||||||
|
- Solo dopo aver configurato i field mappings principali
|
||||||
|
|
||||||
|
#### **DataCoupler.razor.cs** - Gestione Relazioni
|
||||||
|
|
||||||
|
**Campi Aggiunti:**
|
||||||
|
```csharp
|
||||||
|
private List<ExternalIdRelationshipDto> externalIdRelationships = new();
|
||||||
|
private string selectedRelationshipObject = string.Empty;
|
||||||
|
private string selectedExternalIdField = string.Empty;
|
||||||
|
private string selectedRelationshipSourceField = string.Empty;
|
||||||
|
private List<RestEntityInfo> availableRelationshipObjects = new();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Metodi Implementati:**
|
||||||
|
- `OnRelationshipObjectSelected()` - Gestisce selezione oggetto
|
||||||
|
- `AddExternalIdRelationship()` - Aggiunge nuova relazione con validazione
|
||||||
|
- `RemoveExternalIdRelationship()` - Rimuove relazione esistente
|
||||||
|
- `GetExternalIdFieldsForSelectedObject()` - Ottiene campi External ID disponibili
|
||||||
|
- `GetSourceFieldsForRelationship()` - Ottiene campi sorgente per mapping
|
||||||
|
|
||||||
|
**Integrazione Reset/Clear:**
|
||||||
|
- Aggiornato `ClearAllMappings()` per pulire relazioni
|
||||||
|
- Aggiornato `ResetAllState()` per reset completo
|
||||||
|
- Aggiornato `ApplyProfileConfiguration()` per caricare relazioni da profilo
|
||||||
|
|
||||||
|
### 4. Trasformazione Dati
|
||||||
|
|
||||||
|
#### **DataCoupler.razor.cs** - TransformRecordToRestEntity()
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Aggiungi External ID Relationships (per Salesforce)
|
||||||
|
if (externalIdRelationships.Any())
|
||||||
|
{
|
||||||
|
foreach (var relationship in externalIdRelationships)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(relationship.SourceField) &&
|
||||||
|
dbRecord.ContainsKey(relationship.SourceField))
|
||||||
|
{
|
||||||
|
var sourceValue = dbRecord[relationship.SourceField];
|
||||||
|
var transformedValue = TransformValue(sourceValue, relationship.SourceField, relationship.ExternalIdField);
|
||||||
|
|
||||||
|
if (transformedValue != null)
|
||||||
|
{
|
||||||
|
// Formato: { "Account__r": { "Country__c": "US" } }
|
||||||
|
var externalIdObject = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ relationship.ExternalIdField, transformedValue }
|
||||||
|
};
|
||||||
|
|
||||||
|
restData[relationship.RelationshipName] = externalIdObject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **ScheduledProfileExecutionService** - TransformRecordForRest()
|
||||||
|
|
||||||
|
**Modifiche:**
|
||||||
|
- Aggiunto parametro opzionale `List<ExternalIdRelationshipDto>? externalIdRelationships`
|
||||||
|
- Implementata stessa logica di trasformazione per esecuzioni schedulate
|
||||||
|
- Aggiornato `ExecuteDataTransferAsync()` per deserializzare e passare relazioni
|
||||||
|
- Aggiornato `ExecuteDataTransferStandardAsync()` per accettare e usare relazioni
|
||||||
|
- Aggiornato `ExecuteDataTransferWithCompositeAsync()` per supporto Salesforce Composite API
|
||||||
|
|
||||||
|
**Nuovo Metodo:**
|
||||||
|
```csharp
|
||||||
|
private List<ExternalIdRelationshipDto> ParseExternalIdRelationships(string? externalIdRelationshipsJson)
|
||||||
|
{
|
||||||
|
// Deserializza JSON con stesse opzioni di DataCouplerProfileService
|
||||||
|
var options = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
|
};
|
||||||
|
|
||||||
|
return JsonSerializer.Deserialize<List<ExternalIdRelationshipDto>>(externalIdRelationshipsJson, options);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Salvataggio Profili
|
||||||
|
|
||||||
|
#### **Components/ProfileSaver.razor.cs**
|
||||||
|
|
||||||
|
**Modifiche:**
|
||||||
|
- Aggiunto parametro `ExternalIdRelationships`
|
||||||
|
- Incluso nella creazione del DTO per salvataggio profili
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[Parameter]
|
||||||
|
public List<ExternalIdRelationshipDto> ExternalIdRelationships { get; set; } = new();
|
||||||
|
|
||||||
|
// In SaveProfile()
|
||||||
|
ExternalIdRelationships = this.ExternalIdRelationships,
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Discovery REST API
|
||||||
|
|
||||||
|
#### **Data_Coupler/Extensions/DataCoupler/RESTMethod.cs**
|
||||||
|
|
||||||
|
**Modifiche:**
|
||||||
|
- Aggiornato `ConnectToRestApi()` per popolare `availableRelationshipObjects`
|
||||||
|
- Chiamata a `DiscoverEntitiesAsync()` per ottenere dettagli completi oggetti REST
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
try
|
||||||
|
{
|
||||||
|
availableRelationshipObjects = (await currentRestDiscovery.DiscoverEntitiesAsync()).ToList();
|
||||||
|
Logger.LogInformation("Caricati {Count} oggetti REST per External ID Relationships", availableRelationshipObjects.Count);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Impossibile caricare oggetti REST per External ID Relationships");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Migrazione Database
|
||||||
|
|
||||||
|
#### **File Creati:**
|
||||||
|
|
||||||
|
1. **20260203000000_AddExternalIdRelationships.cs**
|
||||||
|
- Migrazione Entity Framework per aggiungere campo `ExternalIdRelationshipsJson`
|
||||||
|
- Tipo: TEXT, MaxLength: 4000, Nullable
|
||||||
|
|
||||||
|
2. **20260203000000_AddExternalIdRelationships.sql**
|
||||||
|
- Script SQL manuale per applicazione diretta se necessario
|
||||||
|
- Include update di `__EFMigrationsHistory`
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE DataCouplerProfiles ADD COLUMN ExternalIdRelationshipsJson TEXT;
|
||||||
|
INSERT INTO __EFMigrationsHistory (MigrationId, ProductVersion)
|
||||||
|
VALUES ('20260203000000_AddExternalIdRelationships', '9.0.0');
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Formato Dati Salesforce
|
||||||
|
|
||||||
|
### Esempio di Trasformazione
|
||||||
|
|
||||||
|
**Configurazione:**
|
||||||
|
- **Relationship Name**: `Account__r`
|
||||||
|
- **Related Object**: `Account`
|
||||||
|
- **External ID Field**: `Country__c`
|
||||||
|
- **Source Field**: `CountryCode` (dalla tabella sorgente)
|
||||||
|
|
||||||
|
**Record Sorgente:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ProductName": "Widget A",
|
||||||
|
"Price": 99.99,
|
||||||
|
"CountryCode": "US"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Record Trasformato per Salesforce:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Name": "Widget A",
|
||||||
|
"Price__c": 99.99,
|
||||||
|
"Account__r": {
|
||||||
|
"Country__c": "US"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vantaggi External ID
|
||||||
|
|
||||||
|
1. **Nessun ID Salesforce Richiesto**: Non serve conoscere l'ID Salesforce dell'Account
|
||||||
|
2. **Lookup Automatico**: Salesforce cerca automaticamente l'Account con `Country__c = "US"`
|
||||||
|
3. **Upsert Intelligente**: Se non trova l'Account, può crearlo automaticamente (se configurato)
|
||||||
|
4. **Manutenzione Semplificata**: I codici esterni sono più stabili degli ID interni
|
||||||
|
|
||||||
|
## 🔄 Flusso Operativo
|
||||||
|
|
||||||
|
### Configurazione Manuale (DataCoupler.razor)
|
||||||
|
|
||||||
|
1. Utente configura connessione sorgente (database/file) e destinazione (Salesforce)
|
||||||
|
2. Sistema scopre automaticamente oggetti REST disponibili
|
||||||
|
3. Utente configura field mappings principali
|
||||||
|
4. Sezione External ID Relationships diventa visibile
|
||||||
|
5. Utente seleziona:
|
||||||
|
- Oggetto correlato (es: Account)
|
||||||
|
- Campo External ID (es: Country__c)
|
||||||
|
- Campo sorgente (es: CountryCode)
|
||||||
|
6. Click su "Aggiungi Relazione" → validazione e aggiunta alla lista
|
||||||
|
7. (Opzionale) Salvataggio come profilo per riutilizzo futuro
|
||||||
|
8. Esecuzione trasferimento → relazioni applicate automaticamente
|
||||||
|
|
||||||
|
### Esecuzione Schedulata (ScheduledProfileExecutionService)
|
||||||
|
|
||||||
|
1. Background service carica profilo dal database
|
||||||
|
2. Deserializza External ID Relationships da JSON
|
||||||
|
3. Estrae dati dalla sorgente
|
||||||
|
4. Trasforma ogni record applicando field mappings + External ID Relationships
|
||||||
|
5. Invia a Salesforce (Standard API o Composite API)
|
||||||
|
6. Gestisce associazioni record e hash per evitare duplicati
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
|
||||||
|
### Scenari di Test Consigliati
|
||||||
|
|
||||||
|
1. **Configurazione UI**
|
||||||
|
- ✅ Selezione oggetti e campi funziona correttamente
|
||||||
|
- ✅ Validazione impedisce relazioni incomplete
|
||||||
|
- ✅ Aggiunta e rimozione relazioni aggiorna UI
|
||||||
|
|
||||||
|
2. **Salvataggio/Caricamento Profili**
|
||||||
|
- ✅ Relazioni salvate correttamente in JSON
|
||||||
|
- ✅ Profilo ricaricato ripristina tutte le relazioni
|
||||||
|
- ✅ Database persiste ExternalIdRelationshipsJson
|
||||||
|
|
||||||
|
3. **Trasformazione Dati**
|
||||||
|
- ✅ Record trasformato include dizionario annidato per relazioni
|
||||||
|
- ✅ Valori null/vuoti gestiti correttamente
|
||||||
|
- ✅ Logging dettagliato per ogni relazione aggiunta
|
||||||
|
|
||||||
|
4. **Esecuzione Schedulata**
|
||||||
|
- ✅ Schedulazione carica e applica relazioni
|
||||||
|
- ✅ Funziona sia con Standard API che Composite API
|
||||||
|
- ✅ Errori gestiti e loggati senza bloccare il flusso
|
||||||
|
|
||||||
|
5. **Integrazione Salesforce**
|
||||||
|
- ✅ Salesforce accetta formato External ID Relationship
|
||||||
|
- ✅ Lookup automatico funziona correttamente
|
||||||
|
- ✅ Record creati con relazioni corrette
|
||||||
|
|
||||||
|
## 📝 Note Implementative
|
||||||
|
|
||||||
|
### Decisioni di Design
|
||||||
|
|
||||||
|
1. **MaxLength JSON: 4000 caratteri**
|
||||||
|
- Ragionamento: Supporta configurazioni complesse senza eccedere limiti SQLite
|
||||||
|
- Alternativa: Se necessario più spazio, può essere aumentato a TEXT illimitato
|
||||||
|
|
||||||
|
2. **Parametro Opzionale in TransformRecordForRest**
|
||||||
|
- Backward compatibility garantita
|
||||||
|
- Chiamate esistenti senza External ID continuano a funzionare
|
||||||
|
|
||||||
|
3. **Filtro Campi External ID**
|
||||||
|
- Logica: `EndsWith("__c") || Name == "Id" || Contains("External")`
|
||||||
|
- Copre la maggior parte dei casi comuni in Salesforce
|
||||||
|
- Personalizzabile se necessario
|
||||||
|
|
||||||
|
4. **Visibilità Condizionale UI**
|
||||||
|
- Solo per Salesforce (verifica `IsSalesforceClient()`)
|
||||||
|
- Solo dopo field mappings configurati (`fieldMappings.Any()`)
|
||||||
|
- Migliora UX evitando confusione per altre API
|
||||||
|
|
||||||
|
### Potenziali Estensioni Future
|
||||||
|
|
||||||
|
1. **Validazione Avanzata**: Verifica esistenza oggetto/campo su Salesforce prima di salvare
|
||||||
|
2. **Multi-Level Relationships**: Supporto per relazioni annidate (es: `Account__r.Owner__r.Name__c`)
|
||||||
|
3. **Relazioni Composite**: Più External ID per stesso oggetto (es: FirstName + LastName)
|
||||||
|
4. **Import/Export Relazioni**: Backup e restore separato delle configurazioni relazioni
|
||||||
|
5. **Template Relazioni**: Libreria di relazioni predefinite per oggetti Salesforce comuni
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Errori Comuni
|
||||||
|
|
||||||
|
**Errore: "External ID field not found"**
|
||||||
|
- Causa: Campo External ID non esiste sull'oggetto Salesforce
|
||||||
|
- Soluzione: Verificare che il campo sia configurato come External ID in Salesforce
|
||||||
|
|
||||||
|
**Errore: "Multiple records found with external ID"**
|
||||||
|
- Causa: External ID non è univoco in Salesforce
|
||||||
|
- Soluzione: Verificare unicità del campo External ID
|
||||||
|
|
||||||
|
**Relazioni Non Applicate**
|
||||||
|
- Causa: `externalIdRelationships` è vuoto
|
||||||
|
- Soluzione: Verificare deserializzazione JSON in profilo
|
||||||
|
|
||||||
|
**UI Non Mostra Sezione Relazioni**
|
||||||
|
- Causa: Condizione visibilità non soddisfatta
|
||||||
|
- Soluzione: Verificare che sia Salesforce e field mappings configurati
|
||||||
|
|
||||||
|
## 📚 Riferimenti
|
||||||
|
|
||||||
|
- [Salesforce External ID Documentation](https://help.salesforce.com/s/articleView?id=sf.fields_about_custom_external_id.htm)
|
||||||
|
- [Salesforce REST API - Insert or Update](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_upsert.htm)
|
||||||
|
- [Salesforce Relationship Fields](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_relationship_fields.htm)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Implementazione Completata**: 3 Febbraio 2026
|
||||||
|
**Framework**: .NET 9.0
|
||||||
|
**Pattern**: Repository + DTO + Service Layer
|
||||||
|
**Database**: SQLite con Entity Framework Core
|
||||||
|
**UI**: Blazor Server con Bootstrap 5
|
||||||
@@ -0,0 +1,345 @@
|
|||||||
|
# Fix Connessione SQL Server con Localhost
|
||||||
|
|
||||||
|
**Data**: 15 Febbraio 2026
|
||||||
|
**Versione**: 2.1+
|
||||||
|
|
||||||
|
## 📋 Problema Risolto
|
||||||
|
|
||||||
|
Il sistema non riusciva a connettersi correttamente a SQL Server quando si utilizzava "localhost" come host, specialmente per:
|
||||||
|
- Named Instances (es. `localhost\SQLEXPRESS`)
|
||||||
|
- LocalDB (es. `(localdb)\MSSQLLocalDB`)
|
||||||
|
- Windows Authentication
|
||||||
|
|
||||||
|
## 🔧 Modifiche Implementate
|
||||||
|
|
||||||
|
### 1. ConnectionStringBuilder - Gestione Intelligente del Server
|
||||||
|
|
||||||
|
**File**: `CredentialManager/Models/CredentialModels.cs`
|
||||||
|
|
||||||
|
#### Miglioramenti:
|
||||||
|
|
||||||
|
**a) Named Instances**
|
||||||
|
- Se l'host contiene `\` (backslash), la porta viene omessa automaticamente
|
||||||
|
- Esempi supportati:
|
||||||
|
- `localhost\SQLEXPRESS`
|
||||||
|
- `.\SQLEXPRESS`
|
||||||
|
- `SERVERNAME\INSTANCE`
|
||||||
|
|
||||||
|
**b) LocalDB**
|
||||||
|
- Se l'host inizia con `(localdb)`, la porta viene omessa
|
||||||
|
- Esempi supportati:
|
||||||
|
- `(localdb)\MSSQLLocalDB`
|
||||||
|
- `(localdb)\v11.0`
|
||||||
|
- `(localdb)\ProjectsV13`
|
||||||
|
|
||||||
|
**c) Localhost con Named Pipes**
|
||||||
|
- Per `localhost`, `.` o `127.0.0.1` con porta 1433 (default), la porta viene omessa
|
||||||
|
- Questo permette a SQL Server di usare Named Pipes invece di TCP/IP per connessioni locali più veloci
|
||||||
|
|
||||||
|
**d) Windows Authentication**
|
||||||
|
- Se username è vuoto, `Integrated` o `Windows`, usa Windows Authentication
|
||||||
|
- Non richiede password quando si usa Windows Authentication
|
||||||
|
- Connection string include `Integrated Security=True`
|
||||||
|
|
||||||
|
#### Codice Modificato:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static string BuildSqlServerConnectionString(DatabaseCredential credential)
|
||||||
|
{
|
||||||
|
var builder = new List<string>();
|
||||||
|
|
||||||
|
// Gestione speciale per SQL Server locale e named instances
|
||||||
|
bool hasInstanceName = credential.Host.Contains('\\') ||
|
||||||
|
credential.Host.StartsWith("(localdb)", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (hasInstanceName)
|
||||||
|
{
|
||||||
|
// Per named instances e LocalDB, non includere la porta
|
||||||
|
builder.Add($"Server={credential.Host}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Per localhost con porta default, ometti la porta per usare Named Pipes
|
||||||
|
if ((credential.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
credential.Host == "." ||
|
||||||
|
credential.Host == "127.0.0.1") && credential.Port == 1433)
|
||||||
|
{
|
||||||
|
builder.Add($"Server={credential.Host}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Per altri casi, usa host,porta
|
||||||
|
builder.Add($"Server={credential.Host},{credential.Port}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Windows Authentication vs SQL Authentication
|
||||||
|
if (string.IsNullOrWhiteSpace(credential.Username) ||
|
||||||
|
credential.Username.Equals("Integrated", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
credential.Username.Equals("Windows", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
builder.Add("Integrated Security=True");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
builder.Add($"User Id={credential.Username}");
|
||||||
|
builder.Add($"Password={credential.Password}");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Add($"Connection Timeout={credential.CommandTimeout}");
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(credential.DatabaseName))
|
||||||
|
builder.Add($"Database={credential.DatabaseName}");
|
||||||
|
|
||||||
|
if (credential.IgnoreSslErrors)
|
||||||
|
builder.Add("TrustServerCertificate=True");
|
||||||
|
|
||||||
|
return string.Join(";", builder);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. UI - Guida Contestuale per SQL Server
|
||||||
|
|
||||||
|
**File**: `Data_Coupler/Pages/CredentialManagement.razor`
|
||||||
|
|
||||||
|
#### Aggiunte:
|
||||||
|
|
||||||
|
**a) Help Text per Host/Server**
|
||||||
|
- Mostra esempi specifici per SQL Server locale:
|
||||||
|
- Named Instance: `localhost\SQLEXPRESS` o `.\SQLEXPRESS`
|
||||||
|
- LocalDB: `(localdb)\MSSQLLocalDB`
|
||||||
|
- Default: `localhost` o `.` (usa porta 1433)
|
||||||
|
|
||||||
|
**b) Nota sulla Porta**
|
||||||
|
- Indica che la porta viene ignorata per named instances e LocalDB
|
||||||
|
|
||||||
|
**c) Guida Windows Authentication**
|
||||||
|
- Nel campo Username: placeholder "o scrivi 'Integrated' per Windows Auth"
|
||||||
|
- Help text: "Per Windows Authentication, scrivi **Integrated** o lascia vuoto"
|
||||||
|
- Nel campo Password: "Non richiesta per Windows Authentication"
|
||||||
|
|
||||||
|
#### Codice Aggiunto:
|
||||||
|
|
||||||
|
```razor
|
||||||
|
@if (currentDatabaseCredential.DatabaseType == DatabaseType.SqlServer)
|
||||||
|
{
|
||||||
|
<div class="form-text">
|
||||||
|
<strong>SQL Server locale:</strong><br/>
|
||||||
|
• Named Instance: <code>localhost\SQLEXPRESS</code> o <code>.\SQLEXPRESS</code><br/>
|
||||||
|
• LocalDB: <code>(localdb)\MSSQLLocalDB</code><br/>
|
||||||
|
• Default: <code>localhost</code> o <code>.</code> (usa porta 1433)
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Validazione Aggiornata
|
||||||
|
|
||||||
|
**File**: `Data_Coupler/Pages/CredentialManagement.razor`
|
||||||
|
|
||||||
|
#### Miglioramenti:
|
||||||
|
|
||||||
|
**a) Validazione Credenziali**
|
||||||
|
- Permette username/password vuoti per SQL Server con Windows Authentication
|
||||||
|
- Riconosce "Integrated" e "Windows" come segnali per Windows Authentication
|
||||||
|
- Validazione più specifica con messaggi di errore appropriati
|
||||||
|
|
||||||
|
#### Codice Modificato:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Per SQL Server, permetti Windows Authentication
|
||||||
|
bool isSqlServerWithWindowsAuth = currentDatabaseCredential.DatabaseType == DatabaseType.SqlServer &&
|
||||||
|
(string.IsNullOrWhiteSpace(currentDatabaseCredential.Username) ||
|
||||||
|
currentDatabaseCredential.Username.Equals("Integrated", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
currentDatabaseCredential.Username.Equals("Windows", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (!isSqlServerWithWindowsAuth)
|
||||||
|
{
|
||||||
|
// Per database che non usano Windows Authentication, richiedi username e password
|
||||||
|
if (string.IsNullOrEmpty(currentDatabaseCredential.Username) ||
|
||||||
|
string.IsNullOrEmpty(currentDatabaseCredential.Password))
|
||||||
|
{
|
||||||
|
await JSRuntime.InvokeVoidAsync("alert",
|
||||||
|
"Username e Password sono obbligatori. Per SQL Server con Windows Authentication, inserisci 'Integrated' come username.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 Guida Utilizzo
|
||||||
|
|
||||||
|
### Scenario 1: SQL Server Express Locale
|
||||||
|
|
||||||
|
**Configurazione Credenziale:**
|
||||||
|
- **Host**: `localhost\SQLEXPRESS` o `.\SQLEXPRESS`
|
||||||
|
- **Porta**: 1433 (ignorata)
|
||||||
|
- **Database**: Nome del database (es. `MyDatabase`)
|
||||||
|
- **Username**: `Integrated` o lascia vuoto
|
||||||
|
- **Password**: Lascia vuoto
|
||||||
|
|
||||||
|
**Connection String Generata:**
|
||||||
|
```
|
||||||
|
Server=localhost\SQLEXPRESS;Integrated Security=True;Connection Timeout=30;Database=MyDatabase;TrustServerCertificate=True
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scenario 2: SQL Server LocalDB
|
||||||
|
|
||||||
|
**Configurazione Credenziale:**
|
||||||
|
- **Host**: `(localdb)\MSSQLLocalDB`
|
||||||
|
- **Porta**: 1433 (ignorata)
|
||||||
|
- **Database**: Nome del database (es. `TestDB`)
|
||||||
|
- **Username**: `Integrated` o lascia vuoto
|
||||||
|
- **Password**: Lascia vuoto
|
||||||
|
|
||||||
|
**Connection String Generata:**
|
||||||
|
```
|
||||||
|
Server=(localdb)\MSSQLLocalDB;Integrated Security=True;Connection Timeout=30;Database=TestDB
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scenario 3: SQL Server Locale con SQL Authentication
|
||||||
|
|
||||||
|
**Configurazione Credenziale:**
|
||||||
|
- **Host**: `localhost`
|
||||||
|
- **Porta**: 1433
|
||||||
|
- **Database**: Nome del database (es. `Production`)
|
||||||
|
- **Username**: `sa` (o un altro utente SQL)
|
||||||
|
- **Password**: Password dell'utente
|
||||||
|
|
||||||
|
**Connection String Generata:**
|
||||||
|
```
|
||||||
|
Server=localhost;User Id=sa;Password=***;Connection Timeout=30;Database=Production;TrustServerCertificate=True
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scenario 4: SQL Server Remoto
|
||||||
|
|
||||||
|
**Configurazione Credenziale:**
|
||||||
|
- **Host**: `sql.example.com`
|
||||||
|
- **Porta**: 1433 (o porta custom, es. 14330)
|
||||||
|
- **Database**: Nome del database
|
||||||
|
- **Username**: Utente SQL
|
||||||
|
- **Password**: Password
|
||||||
|
|
||||||
|
**Connection String Generata:**
|
||||||
|
```
|
||||||
|
Server=sql.example.com,1433;User Id=username;Password=***;Connection Timeout=30;Database=DBName;TrustServerCertificate=True
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scenario 5: SQL Server con Instance Name Remoto
|
||||||
|
|
||||||
|
**Configurazione Credenziale:**
|
||||||
|
- **Host**: `server.domain.com\PRODUCTION`
|
||||||
|
- **Porta**: 1433 (ignorata)
|
||||||
|
- **Database**: Nome del database
|
||||||
|
- **Username**: Utente SQL
|
||||||
|
- **Password**: Password
|
||||||
|
|
||||||
|
**Connection String Generata:**
|
||||||
|
```
|
||||||
|
Server=server.domain.com\PRODUCTION;User Id=username;Password=***;Connection Timeout=30;Database=DBName;TrustServerCertificate=True
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 Troubleshooting
|
||||||
|
|
||||||
|
### Problema: "A network-related or instance-specific error"
|
||||||
|
|
||||||
|
**Possibili Cause:**
|
||||||
|
1. **SQL Server Browser non in esecuzione** (per named instances)
|
||||||
|
- Soluzione: Avvia il servizio "SQL Server Browser" da services.msc
|
||||||
|
|
||||||
|
2. **TCP/IP non abilitato**
|
||||||
|
- Soluzione: SQL Server Configuration Manager → Protocols → Enable TCP/IP
|
||||||
|
|
||||||
|
3. **Named Instance non specificata**
|
||||||
|
- Soluzione: Usa `localhost\SQLEXPRESS` invece di solo `localhost`
|
||||||
|
|
||||||
|
4. **Firewall blocca la porta**
|
||||||
|
- Soluzione: Aggiungi eccezione firewall per SQL Server
|
||||||
|
|
||||||
|
### Problema: "Login failed for user"
|
||||||
|
|
||||||
|
**Possibili Cause:**
|
||||||
|
1. **Windows Authentication richiesta ma SQL Auth specificata**
|
||||||
|
- Soluzione: Usa username `Integrated` o lascialo vuoto
|
||||||
|
|
||||||
|
2. **SQL Authentication non abilitata**
|
||||||
|
- Soluzione: SQL Server Management Studio → Proprietà Server → Security → SQL Server and Windows Authentication mode
|
||||||
|
|
||||||
|
3. **Password errata**
|
||||||
|
- Soluzione: Verifica la password
|
||||||
|
|
||||||
|
### Problema: "Cannot open database"
|
||||||
|
|
||||||
|
**Possibili Cause:**
|
||||||
|
1. **Database non esiste**
|
||||||
|
- Soluzione: Verifica il nome del database o lascia il campo vuoto per connetterti solo al server
|
||||||
|
|
||||||
|
2. **Permessi insufficienti**
|
||||||
|
- Soluzione: Verifica che l'utente abbia accesso al database
|
||||||
|
|
||||||
|
## ✅ Test di Connessione
|
||||||
|
|
||||||
|
Dopo aver configurato la credenziale, usa il pulsante **"Testa Connessione"** per verificare:
|
||||||
|
- ✅ Connection string corretta
|
||||||
|
- ✅ SQL Server raggiungibile
|
||||||
|
- ✅ Autenticazione riuscita
|
||||||
|
- ✅ Database accessibile (se specificato)
|
||||||
|
|
||||||
|
Il test mostra:
|
||||||
|
- Versione SQL Server
|
||||||
|
- Host e porta usati
|
||||||
|
- Database connesso
|
||||||
|
- Timeout configurato
|
||||||
|
|
||||||
|
## 📝 Note Tecniche
|
||||||
|
|
||||||
|
### Differenze TCP/IP vs Named Pipes
|
||||||
|
|
||||||
|
**Named Pipes** (preferito per localhost):
|
||||||
|
- Più veloce per connessioni locali
|
||||||
|
- Non richiede SQL Server Browser
|
||||||
|
- Usa IPC invece di network stack
|
||||||
|
- Sintassi: `Server=localhost` o `Server=.`
|
||||||
|
|
||||||
|
**TCP/IP** (richiesto per remote):
|
||||||
|
- Richiesto per connessioni remote
|
||||||
|
- Richiede porta specifica
|
||||||
|
- Richiede SQL Server Browser per named instances
|
||||||
|
- Sintassi: `Server=hostname,port`
|
||||||
|
|
||||||
|
### Windows Authentication vs SQL Authentication
|
||||||
|
|
||||||
|
**Windows Authentication**:
|
||||||
|
- ✅ Più sicuro (usa credenziali Windows)
|
||||||
|
- ✅ No password nel codice
|
||||||
|
- ✅ Single Sign-On
|
||||||
|
- ❌ Richiede domain trust per remote
|
||||||
|
|
||||||
|
**SQL Authentication**:
|
||||||
|
- ✅ Funziona sempre (anche cross-domain)
|
||||||
|
- ✅ Credenziali specifiche per SQL Server
|
||||||
|
- ❌ Password nel connection string
|
||||||
|
- ❌ Deve essere abilitato in SQL Server
|
||||||
|
|
||||||
|
## 🔄 Retrocompatibilità
|
||||||
|
|
||||||
|
Le modifiche sono completamente retrocompatibili:
|
||||||
|
- ✅ Connection string esistenti continuano a funzionare
|
||||||
|
- ✅ Credenziali già salvate non richiedono modifiche
|
||||||
|
- ✅ Comportamento default invariato per server remoti
|
||||||
|
- ✅ Nessuna migrazione database richiesta
|
||||||
|
|
||||||
|
## 📊 Impatto Performance
|
||||||
|
|
||||||
|
**Miglioramenti**:
|
||||||
|
- 🚀 Named Pipes più veloce di TCP/IP per localhost
|
||||||
|
- 🚀 Riduzione overhead network stack
|
||||||
|
- 🚀 Connection pooling più efficiente
|
||||||
|
|
||||||
|
**Nessun Impatto Negativo**:
|
||||||
|
- ✅ Server remoti usano sempre TCP/IP (comportamento corretto)
|
||||||
|
- ✅ Connection string ottimizzate per scenario specifico
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Sviluppatore**: Alessio Dalsanto
|
||||||
|
**Issue**: Connessione localhost SQL Server
|
||||||
|
**Status**: ✅ Risolto
|
||||||
Reference in New Issue
Block a user