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:
@@ -146,6 +146,19 @@ public partial class DataCoupler : ComponentBase
|
||||
isRestConnected = true;
|
||||
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -474,12 +474,27 @@ else
|
||||
<label class="form-label">Host/Server *</label>
|
||||
<InputText class="form-control" @bind-Value="currentDatabaseCredential.Host"
|
||||
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 class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Porta *</label>
|
||||
<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>
|
||||
@@ -495,13 +510,26 @@ else
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<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 class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Password *</label>
|
||||
<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>
|
||||
@@ -994,13 +1022,28 @@ else
|
||||
else
|
||||
{
|
||||
// Altri database: validazione standard (Host, Username, Password)
|
||||
if (string.IsNullOrEmpty(currentDatabaseCredential.Host) ||
|
||||
string.IsNullOrEmpty(currentDatabaseCredential.Username) ||
|
||||
string.IsNullOrEmpty(currentDatabaseCredential.Password))
|
||||
// Per SQL Server, permetti Windows Authentication (username vuoto o "Integrated")
|
||||
bool isSqlServerWithWindowsAuth = currentDatabaseCredential.DatabaseType == DatabaseType.SqlServer &&
|
||||
(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;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -974,6 +974,119 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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())
|
||||
{
|
||||
<div class="mt-4">
|
||||
@@ -1153,6 +1266,8 @@
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
|
||||
<div class="mt-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
@@ -1198,7 +1313,8 @@
|
||||
DestinationCredentialId="@(GetCurrentDestinationCredentialIdAsync().Result)"
|
||||
DestinationCredentialName="@selectedRestCredential"
|
||||
DestinationEndpoint="@selectedRestEntity?.Name"
|
||||
FieldMappings="@GetCurrentFieldMappings()"
|
||||
FieldMappings="@GetCurrentFieldMappings()"
|
||||
ExternalIdRelationships="@externalIdRelationships"
|
||||
SourceKeyField="@sourceKeyField"
|
||||
UseRecordAssociations="@useRecordAssociations"
|
||||
OnProfileSaved="@OnProfileSaved" />
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -164,18 +164,25 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
|
||||
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
|
||||
bool useSalesforceComposite = restClient is DataConnection.REST.Implementations.SalesforceServiceClient;
|
||||
|
||||
if (useSalesforceComposite)
|
||||
{
|
||||
_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
|
||||
{
|
||||
_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)
|
||||
@@ -363,6 +370,53 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
|
||||
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>
|
||||
/// Ottiene tutti i record dal database
|
||||
/// </summary>
|
||||
@@ -631,6 +685,7 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
|
||||
RestEntitySummary restEntity,
|
||||
RestApiCredential restCredential,
|
||||
Dictionary<string, string> fieldMappings,
|
||||
List<ExternalIdRelationshipDto> externalIdRelationships,
|
||||
bool enableDeletionSync = false)
|
||||
{
|
||||
_logger.LogInformation("Iniziando trasferimento dati standard per {RecordCount} record - DeletionSync: {DeletionSync}",
|
||||
@@ -644,8 +699,8 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
|
||||
{
|
||||
try
|
||||
{
|
||||
// 1. Trasforma il record utilizzando i field mappings
|
||||
var restData = TransformRecordForRest(record, fieldMappings);
|
||||
// 1. Trasforma il record utilizzando i field mappings e External ID Relationships
|
||||
var restData = TransformRecordForRest(record, fieldMappings, externalIdRelationships);
|
||||
|
||||
// 2. Gestione associazioni record se abilitata
|
||||
string? entityId = null;
|
||||
@@ -755,6 +810,7 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
|
||||
RestEntitySummary restEntity,
|
||||
RestApiCredential restCredential,
|
||||
Dictionary<string, string> fieldMappings,
|
||||
List<ExternalIdRelationshipDto> externalIdRelationships,
|
||||
bool enableDeletionSync = false)
|
||||
{
|
||||
_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))
|
||||
{
|
||||
_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
|
||||
@@ -794,8 +850,8 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
|
||||
var record = indexedRecord.Record;
|
||||
var recordNumber = indexedRecord.RecordNumber;
|
||||
|
||||
// Trasforma il record in base ai mapping (operazione locale, thread-safe)
|
||||
var restData = TransformRecordForRest(record, fieldMappings);
|
||||
// Trasforma il record in base ai mapping e External ID Relationships (operazione locale, thread-safe)
|
||||
var restData = TransformRecordForRest(record, fieldMappings, externalIdRelationships);
|
||||
|
||||
// Genera la chiave sorgente e l'hash dei dati per questo record (include MAPPING_SIGNATURE)
|
||||
var sourceKey = GenerateSourceKey(record, profile.SourceKeyField);
|
||||
@@ -1085,7 +1141,10 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
|
||||
/// <summary>
|
||||
/// Trasforma un record sorgente in formato REST utilizzando i field mappings
|
||||
/// </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>();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user