Fix: Risolto double-mapping negli External ID Relationships per Salesforce

- Implementata funzionalità completa External ID Relationships nell'interfaccia di mapping
- Corretto bug double-mapping: i campi sorgente usati per External ID non vengono più inclusi nei mapping normali
- Risolto errore MALFORMED_ID causato dall'invio duplicato di campi come proprietà dirette e nested objects
- Implementata logica corretta per relationship names: oggetti standard usano il nome diretto, custom objects usano suffisso __r
- Aggiunta UI a 3 colonne (Object, External ID Field, Source Field) per configurazione External ID Relationships
- Migrazione database per supporto External ID Relationships nei profili
- Aggiornato ProfileSaver.razor.cs per salvare/caricare External ID Relationships
- Aggiornato ScheduledProfileExecutionService.cs per gestire External ID nelle esecuzioni schedulate
- Formato JSON output corretto: { 'Account': { 'CardCode__c': 'V50000' } }

Documentazione: EXTERNAL_ID_RELATIONSHIPS_IMPLEMENTATION.md
This commit is contained in:
2026-02-15 18:44:15 +01:00
parent ed5316fbdf
commit 483eb7b407
16 changed files with 1923 additions and 21 deletions
+201
View File
@@ -54,6 +54,13 @@ public partial class DataCoupler : ComponentBase
private Dictionary<string, string> fieldMappings = new(); // DbColumn -> RestProperty
private HashSet<string> keyFields = new(); // REST properties marked as keys
private string selectedDbColumn = "";
// 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
private string sourceKeyField = ""; // Campo che identifica univocamente il record sorgente
@@ -374,6 +381,33 @@ public partial class DataCoupler : ComponentBase
{
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
useRecordAssociations = profile.UseRecordAssociations;
@@ -688,6 +722,7 @@ public partial class DataCoupler : ComponentBase
ResetDestinationState();
fieldMappings.Clear();
keyFields.Clear();
externalIdRelationships.Clear(); // Reset relazioni
transferResults.Clear();
transferMessage = "";
}
@@ -1316,6 +1351,9 @@ public partial class DataCoupler : ComponentBase
Logger.LogInformation("Rimosso mapping specifico per campo: {DbColumn}", dbColumn);
}
}
Logger.LogInformation("Rimosso mapping specifico per campo: {DbColumn}", dbColumn);
}
}
private void ClearAllMappings()
{
@@ -1325,8 +1363,128 @@ public partial class DataCoupler : ComponentBase
sourceKeyField = "";
transferMessage = "";
transferMessageType = "";
externalIdRelationships.Clear(); // Pulisce anche le relazioni
Logger.LogInformation("Tutti i mapping 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()
{
@@ -1943,11 +2101,25 @@ public partial class DataCoupler : ComponentBase
{
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();
foreach (var mapping in fieldMappings)
{
string dbColumn = mapping.Key;
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))
{
var value = dbRecord[dbColumn];
@@ -1962,6 +2134,35 @@ public partial class DataCoupler : ComponentBase
}
}
// 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}",
string.Join(", ", dbRecord.Keys),
string.Join(", ", restData.Keys));