feat: Implementa gestione intelligente della chiave sorgente con rilevamento PK

- Aggiunge rilevamento automatico Primary Key per connessioni database
- Rimuove completamente il fallback automatico per lato sorgente
- Implementa selezione manuale obbligatoria per file e sorgenti non-DB
- Migliora UI con suggerimenti intelligenti e feedback visivo
- Aggiunge validazione multi-livello (UI, pre-transfer, runtime)
- Introduce metodo GetPrimaryKeyFieldAsync in IDatabaseManager
- Modifica GenerateSourceKey per richiedere sempre campo specifico
- Implementa controllo IsTransferButtonEnabled per validazione form

Breaking changes:
- La generazione automatica delle chiavi sorgente è stata rimossa
- Il campo chiave sorgente è ora obbligatorio quando si usa il sistema associazioni

Fixes: Risolve problema di discovery schema vuoto con selezione database
This commit is contained in:
2025-06-28 02:05:59 +02:00
parent 207d6fc845
commit 51c61eabf7
29 changed files with 2748 additions and 104 deletions
@@ -135,6 +135,36 @@ namespace DataConnection.REST.Implementations
return await CreateEntityAsync(entityName, entityData, cancellationToken);
}
public virtual async Task<List<Dictionary<string, object>>> FindEntitiesByKeysAsync(string entityName, Dictionary<string, object> keyFields, CancellationToken cancellationToken = default)
{
// Default implementation - returns empty list
// Derived classes should override this method for service-specific entity search logic
await Task.CompletedTask;
return new List<Dictionary<string, object>>();
}
public virtual async Task<bool> DeleteEntityAsync(string entityName, string entityId, CancellationToken cancellationToken = default)
{
// Default implementation - returns false (not supported)
// Derived classes should override this method for service-specific entity deletion logic
await Task.CompletedTask;
return false;
} public virtual async Task<Dictionary<string, object>?> UpdateEntityAsync(string entityName, string entityId, Dictionary<string, object> entityData, CancellationToken cancellationToken = default)
{
// Default implementation - returns null (not supported)
// Derived classes should override this method for service-specific entity update logic
await Task.CompletedTask;
return null;
}
public virtual async Task<List<Dictionary<string, object>>> FindEntitiesByRequiredFieldsAsync(string entityName, Dictionary<string, object> requiredFields, CancellationToken cancellationToken = default)
{
// Default implementation - returns empty list (not supported)
// Derived classes should override this method for service-specific duplicate detection logic
await Task.CompletedTask;
return new List<Dictionary<string, object>>();
}
public virtual async Task<bool> AuthenticateAsync(CancellationToken cancellationToken = default)
{
// Default implementation for basic authentication (already handled in ConfigureHttpClient)
@@ -481,6 +481,176 @@ namespace DataConnection.REST.Implementations
Console.WriteLine($"Error during Salesforce entity upsert: {ex.Message}");
return null;
}
} /// <summary>
/// Finds entities by their key fields in Salesforce.
/// </summary>
/// <param name="entityName">The name of the SObject to search (e.g., "Account", "Contact").</param>
/// <param name="keyFields">The key fields and their values to match.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A list of matching entities or an empty list if none found.</returns>
public override async Task<List<Dictionary<string, object>>> FindEntitiesByKeysAsync(string entityName, Dictionary<string, object> keyFields, CancellationToken cancellationToken = default)
{
try
{
Console.WriteLine($"--- Starting Salesforce Entity Search: {entityName} ---");
Console.WriteLine($"Key Fields: {string.Join(", ", keyFields.Select(kvp => $"{kvp.Key}={kvp.Value}"))}");
if (!await EnsureAuthenticatedAsync(cancellationToken))
{
Console.WriteLine("Authentication failed for entity search");
return new List<Dictionary<string, object>>();
}
// Costruisci la query SOQL
var whereConditions = keyFields.Select(kvp =>
{
var value = kvp.Value?.ToString() ?? "";
// Se il valore è una stringa, aggiungi le virgolette
if (kvp.Value is string)
{
value = $"'{value.Replace("'", "\\'")}'"; // Escape delle virgolette
}
return $"{kvp.Key} = {value}";
});
var query = $"SELECT Id FROM {entityName} WHERE {string.Join(" AND ", whereConditions)}";
Console.WriteLine($"SOQL Query: {query}");
var encodedQuery = Uri.EscapeDataString(query);
var queryEndpoint = $"/services/data/v59.0/query/?q={encodedQuery}"; var response = await GetAsync<SalesforceQueryResponse>(queryEndpoint, cancellationToken);
if (response?.Records != null)
{
var results = response.Records.Select(record =>
record as Dictionary<string, object> ?? new Dictionary<string, object>()
).ToList();
Console.WriteLine($"Found {results.Count} entities matching the key fields");
return results;
}
Console.WriteLine("No entities found matching the key fields");
return new List<Dictionary<string, object>>();
}
catch (Exception ex)
{
Console.WriteLine($"Error during Salesforce entity search: {ex.Message}");
return new List<Dictionary<string, object>>();
}
} /// <summary>
/// Deletes an entity in Salesforce by its ID.
/// </summary>
/// <param name="entityName">The name of the SObject (e.g., "Account", "Contact").</param>
/// <param name="entityId">The ID of the entity to delete.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>True if deletion was successful, false otherwise.</returns>
public override async Task<bool> DeleteEntityAsync(string entityName, string entityId, CancellationToken cancellationToken = default)
{
try
{
Console.WriteLine($"--- Starting Salesforce Entity Delete: {entityName}/{entityId} ---");
if (!await EnsureAuthenticatedAsync(cancellationToken))
{
Console.WriteLine("Authentication failed for entity deletion");
return false;
}
var deleteEndpoint = $"/services/data/v59.0/sobjects/{entityName}/{entityId}";
// Salesforce usa DELETE HTTP method per eliminare record
var request = new HttpRequestMessage(HttpMethod.Delete, $"{_instanceUrl}{deleteEndpoint}");
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _accessToken);
var response = await _httpClient.SendAsync(request, cancellationToken);
if (response.IsSuccessStatusCode)
{
Console.WriteLine($"Entity {entityName}/{entityId} deleted successfully");
return true;
}
else
{
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken);
Console.WriteLine($"Failed to delete entity {entityName}/{entityId}. Status: {response.StatusCode}, Error: {errorContent}");
return false;
}
}
catch (Exception ex)
{
Console.WriteLine($"Error during Salesforce entity deletion: {ex.Message}");
return false;
}
}
/// <summary>
/// Updates an existing entity in Salesforce by its ID.
/// </summary>
/// <param name="entityName">The name of the SObject (e.g., "Account", "Contact").</param>
/// <param name="entityId">The ID of the entity to update.</param>
/// <param name="entityData">The data to update as key-value pairs.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The updated entity data or null if update failed.</returns>
public override async Task<Dictionary<string, object>?> UpdateEntityAsync(string entityName, string entityId, Dictionary<string, object> entityData, CancellationToken cancellationToken = default)
{
try
{
Console.WriteLine($"--- Starting Salesforce Entity Update: {entityName}/{entityId} ---");
if (!await EnsureAuthenticatedAsync(cancellationToken))
{
Console.WriteLine("Authentication failed for entity update");
return null;
}
var updateEndpoint = $"/services/data/v59.0/sobjects/{entityName}/{entityId}";
// Salesforce usa PATCH HTTP method per aggiornare record
var request = new HttpRequestMessage(HttpMethod.Patch, $"{_instanceUrl}{updateEndpoint}");
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _accessToken);
request.Content = JsonContent.Create(entityData);
var response = await _httpClient.SendAsync(request, cancellationToken);
if (response.IsSuccessStatusCode)
{
Console.WriteLine($"Entity {entityName}/{entityId} updated successfully");
// Ritorna i dati aggiornati includendo l'ID
var updatedData = new Dictionary<string, object>(entityData)
{
["Id"] = entityId
};
return updatedData;
}
else
{
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken);
Console.WriteLine($"Failed to update entity {entityName}/{entityId}. Status: {response.StatusCode}, Error: {errorContent}");
return null;
}
}
catch (Exception ex)
{
Console.WriteLine($"Error during Salesforce entity update: {ex.Message}");
return null;
}
}
/// <summary>
/// Ensures the client is authenticated, attempting to authenticate if not already authenticated.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>True if authentication is successful, false otherwise.</returns>
private async Task<bool> EnsureAuthenticatedAsync(CancellationToken cancellationToken = default)
{
if (IsAuthenticated())
{
return true;
}
Console.WriteLine("Client not authenticated, attempting to authenticate...");
return await AuthenticateAsync(cancellationToken);
}
// --- Nested classes for deserializing Salesforce responses ---
@@ -541,7 +711,77 @@ namespace DataConnection.REST.Implementations
public string Label { get; set; } = string.Empty;
[JsonPropertyName("fields")]
public List<SalesforceField> Fields { get; set; } = new List<SalesforceField>();
public List<SalesforceField> Fields { get; set; } = new List<SalesforceField>(); } /// <summary>
/// Finds entities by required fields to detect duplicates.
/// </summary>
/// <param name="entityName">The name of the entity to search.</param>
/// <param name="requiredFields">The required fields and their values to search for.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A list of matching entities.</returns>
public override async Task<List<Dictionary<string, object>>> FindEntitiesByRequiredFieldsAsync(string entityName, Dictionary<string, object> requiredFields, CancellationToken cancellationToken = default)
{
try
{
Console.WriteLine($"--- Searching for duplicates in {entityName} by required fields ---");
if (!await EnsureAuthenticatedAsync(cancellationToken))
{
Console.WriteLine("Authentication failed for required fields search");
return new List<Dictionary<string, object>>();
}
if (!requiredFields.Any())
{
Console.WriteLine("No required fields provided for duplicate search");
return new List<Dictionary<string, object>>();
} // Build WHERE clause with required fields
var whereConditions = new List<string>();
foreach (var field in requiredFields)
{
if (field.Value != null)
{
var value = field.Value.ToString();
// Escape single quotes in string values
if (field.Value is string stringValue)
{
value = stringValue.Replace("'", "\\'");
whereConditions.Add($"{field.Key} = '{value}'");
}
else
{
whereConditions.Add($"{field.Key} = {value}");
}
}
}
if (!whereConditions.Any())
{
Console.WriteLine("No valid field values provided for duplicate search");
return new List<Dictionary<string, object>>();
}
var whereClause = string.Join(" AND ", whereConditions);
var query = $"SELECT Id, {string.Join(", ", requiredFields.Keys)} FROM {entityName} WHERE {whereClause}";
Console.WriteLine($"Executing duplicate search query: {query}");
var queryEndpoint = $"/services/data/v59.0/query?q={Uri.EscapeDataString(query)}";
var response = await GetAsync<SalesforceQueryResponse>($"{_instanceUrl}{queryEndpoint}", cancellationToken);
if (response?.Records != null)
{
Console.WriteLine($"Found {response.Records.Count} potential duplicates for required fields: {string.Join(", ", requiredFields.Select(kv => $"{kv.Key}={kv.Value}"))}");
return response.Records;
}
Console.WriteLine("No duplicates found");
return new List<Dictionary<string, object>>();
}
catch (Exception ex)
{
Console.WriteLine($"Error during required fields search: {ex.Message}");
return new List<Dictionary<string, object>>();
}
}
private class SalesforceField
@@ -564,5 +804,11 @@ namespace DataConnection.REST.Implementations
[JsonPropertyName("unique")]
public bool Unique { get; set; }
}
private class SalesforceQueryResponse
{
[JsonPropertyName("records")]
public List<Dictionary<string, object>> Records { get; set; } = new List<Dictionary<string, object>>();
}
}
}