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:
@@ -56,4 +56,14 @@ public interface IDataConnectionCredentialService
|
||||
Task<(bool Success, string Message)> TestSapB1ConnectionAsync(SapB1ServiceLayerCredential credential);
|
||||
Task<(bool Success, string Message)> TestSalesforceConnectionAsync(string credentialName);
|
||||
Task<(bool Success, string Message)> TestSalesforceConnectionAsync(SalesforceCredential credential);
|
||||
|
||||
// Record associations
|
||||
Task<int> SaveRecordAssociationAsync(RecordAssociation association);
|
||||
Task<RecordAssociation?> FindRecordAssociationAsync(string sourceName, string sourceKey, string destinationEntity);
|
||||
Task<List<RecordAssociation>> GetRecordAssociationsBySourceAsync(string sourceName, string sourceType);
|
||||
Task<List<RecordAssociation>> GetRecordAssociationsByDestinationAsync(string destinationEntity, string restCredentialName);
|
||||
Task<List<RecordAssociation>> GetAllActiveRecordAssociationsAsync();
|
||||
Task<bool> UpdateRecordAssociationAsync(RecordAssociation association);
|
||||
Task<bool> DeactivateRecordAssociationAsync(int id);
|
||||
Task<bool> DeleteRecordAssociationAsync(int id);
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ public static class ServiceCollectionExtensions
|
||||
// Aggiungi i servizi base di CredentialManager
|
||||
services.AddCredentialManager(databasePath);
|
||||
|
||||
// Aggiungi il servizio di gestione associazioni record
|
||||
services.AddScoped<IRecordAssociationService, RecordAssociationService>();
|
||||
|
||||
// Aggiungi il servizio di integrazione DataConnection
|
||||
services.AddScoped<IDataConnectionCredentialService, DataConnectionCredentialService>();
|
||||
|
||||
|
||||
@@ -15,13 +15,16 @@ namespace DataConnection.CredentialManagement.Services;
|
||||
public class DataConnectionCredentialService : IDataConnectionCredentialService
|
||||
{
|
||||
private readonly ICredentialService _credentialService;
|
||||
private readonly IRecordAssociationService _recordAssociationService;
|
||||
private readonly ILogger<DataConnectionCredentialService> _logger;
|
||||
|
||||
public DataConnectionCredentialService(
|
||||
ICredentialService credentialService,
|
||||
IRecordAssociationService recordAssociationService,
|
||||
ILogger<DataConnectionCredentialService> logger)
|
||||
{
|
||||
_credentialService = credentialService;
|
||||
_recordAssociationService = recordAssociationService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -855,4 +858,48 @@ public class DataConnectionCredentialService : IDataConnectionCredentialService
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Record Associations
|
||||
|
||||
public async Task<int> SaveRecordAssociationAsync(RecordAssociation association)
|
||||
{
|
||||
return await _recordAssociationService.SaveAssociationAsync(association);
|
||||
}
|
||||
|
||||
public async Task<RecordAssociation?> FindRecordAssociationAsync(string sourceName, string sourceKey, string destinationEntity)
|
||||
{
|
||||
return await _recordAssociationService.FindAssociationAsync(sourceName, sourceKey, destinationEntity);
|
||||
}
|
||||
|
||||
public async Task<List<RecordAssociation>> GetRecordAssociationsBySourceAsync(string sourceName, string sourceType)
|
||||
{
|
||||
return await _recordAssociationService.GetAssociationsBySourceAsync(sourceName, sourceType);
|
||||
}
|
||||
|
||||
public async Task<List<RecordAssociation>> GetRecordAssociationsByDestinationAsync(string destinationEntity, string restCredentialName)
|
||||
{
|
||||
return await _recordAssociationService.GetAssociationsByDestinationAsync(destinationEntity, restCredentialName);
|
||||
}
|
||||
|
||||
public async Task<List<RecordAssociation>> GetAllActiveRecordAssociationsAsync()
|
||||
{
|
||||
return await _recordAssociationService.GetAllActiveAssociationsAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateRecordAssociationAsync(RecordAssociation association)
|
||||
{
|
||||
return await _recordAssociationService.UpdateAssociationAsync(association);
|
||||
}
|
||||
|
||||
public async Task<bool> DeactivateRecordAssociationAsync(int id)
|
||||
{
|
||||
return await _recordAssociationService.DeactivateAssociationAsync(id);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteRecordAssociationAsync(int id)
|
||||
{
|
||||
return await _recordAssociationService.DeleteAssociationAsync(id);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -109,35 +109,26 @@ public class EFCoreDatabaseManager : IDatabaseManager
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.WriteLine($"[DEBUG] Iniziando GetDatabaseSchemaAsync - DatabaseType: {_options.DatabaseType}");
|
||||
|
||||
// Assicurarsi che il contesto sia connesso
|
||||
await _context.Database.OpenConnectionAsync();
|
||||
Console.WriteLine($"[DEBUG] Connessione al database aperta. Connection string: {_context.Database.GetConnectionString()}");
|
||||
|
||||
// Usa la factory per ottenere il provider appropriato in base al tipo di database
|
||||
var schemaProvider = DatabaseSchemaProviderFactory.CreateProvider(_options.DatabaseType);
|
||||
Console.WriteLine($"[DEBUG] Schema provider creato: {schemaProvider.GetType().Name}");
|
||||
|
||||
// Usa il provider per ottenere lo schema
|
||||
var result = await schemaProvider.GetDatabaseSchemaAsync(_context.Database.GetConnectionString());
|
||||
Console.WriteLine($"[DEBUG] Schema ottenuto. Numero tabelle: {result?.Count ?? 0}");
|
||||
|
||||
if (result != null && result.Count > 0)
|
||||
{
|
||||
foreach (var table in result.Take(3))
|
||||
{
|
||||
Console.WriteLine($"[DEBUG] Tabella: {table.Key}, Colonne: {table.Value?.Count() ?? 0}");
|
||||
}
|
||||
}
|
||||
var connectionString = _context.Database.GetConnectionString();
|
||||
if (connectionString == null)
|
||||
throw new InvalidOperationException("Connection string is null");
|
||||
|
||||
var result = await schemaProvider.GetDatabaseSchemaAsync(connectionString);
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Errore nel recupero dello schema del database: {ex.Message}");
|
||||
Console.WriteLine($"[DEBUG] Stack trace: {ex.StackTrace}");
|
||||
throw; }
|
||||
throw;
|
||||
}
|
||||
} public async Task<IEnumerable<Dictionary<string, object>>> GetAllRecordsAsync(string tableName)
|
||||
{
|
||||
try
|
||||
@@ -146,7 +137,8 @@ public class EFCoreDatabaseManager : IDatabaseManager
|
||||
|
||||
// Usa la stessa connection string utilizzata per il discovery dello schema
|
||||
var connectionString = _context.Database.GetConnectionString();
|
||||
Console.WriteLine($"[DEBUG] GetAllRecordsAsync - Using connection string: {connectionString?.Substring(0, Math.Min(50, connectionString?.Length ?? 0))}...");
|
||||
if (connectionString == null)
|
||||
throw new InvalidOperationException("Connection string is null");
|
||||
|
||||
// Determina il tipo di connessione in base al DatabaseType
|
||||
using var connection = CreateConnection(connectionString);
|
||||
@@ -171,7 +163,6 @@ public class EFCoreDatabaseManager : IDatabaseManager
|
||||
}
|
||||
|
||||
command.CommandText = $"SELECT TOP 1000 * FROM {tableReference}";
|
||||
Console.WriteLine($"[DEBUG] GetAllRecordsAsync - Query: {command.CommandText}");
|
||||
|
||||
using var reader = await command.ExecuteReaderAsync();
|
||||
|
||||
@@ -183,13 +174,12 @@ public class EFCoreDatabaseManager : IDatabaseManager
|
||||
{
|
||||
var columnName = reader.GetName(i);
|
||||
var value = reader.IsDBNull(i) ? null : reader.GetValue(i);
|
||||
record[columnName] = value;
|
||||
record[columnName] = value!;
|
||||
}
|
||||
|
||||
records.Add(record);
|
||||
}
|
||||
|
||||
Console.WriteLine($"[DEBUG] GetAllRecordsAsync - Tabella: {tableName}, Record ottenuti: {records.Count}");
|
||||
return records;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -199,6 +189,114 @@ public class EFCoreDatabaseManager : IDatabaseManager
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<string>> GetAvailableDatabasesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var connectionString = _context.Database.GetConnectionString();
|
||||
if (connectionString == null)
|
||||
throw new InvalidOperationException("Connection string is null");
|
||||
|
||||
// Crea una connessione al server (senza specificare il database)
|
||||
var serverConnectionString = GetServerConnectionString(connectionString);
|
||||
|
||||
using var connection = CreateConnection(serverConnectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
using var command = connection.CreateCommand();
|
||||
|
||||
// Query per ottenere i database disponibili (esclude quelli di sistema)
|
||||
command.CommandText = @"
|
||||
SELECT name
|
||||
FROM sys.databases
|
||||
WHERE state_desc = 'ONLINE'
|
||||
AND name NOT IN ('master', 'tempdb', 'model', 'msdb', 'distribution')
|
||||
ORDER BY name";
|
||||
|
||||
var databases = new List<string>();
|
||||
using var reader = await command.ExecuteReaderAsync();
|
||||
|
||||
while (await reader.ReadAsync())
|
||||
{
|
||||
databases.Add(reader.GetString(0));
|
||||
}
|
||||
|
||||
return databases;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Errore nell'ottenere la lista dei database: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ChangeDatabaseAsync(string databaseName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var currentConnectionString = _context.Database.GetConnectionString();
|
||||
if (currentConnectionString == null)
|
||||
throw new InvalidOperationException("Connection string is null");
|
||||
|
||||
// Crea una nuova connection string con il database specificato
|
||||
var newConnectionString = UpdateConnectionStringDatabase(currentConnectionString, databaseName);
|
||||
|
||||
// Ricrea il contesto con la nuova connection string
|
||||
var optionsBuilder = new DbContextOptionsBuilder<ExistingDatabaseContext>();
|
||||
|
||||
switch (_options.DatabaseType)
|
||||
{
|
||||
case Enums.DatabaseType.SqlServer:
|
||||
optionsBuilder.UseSqlServer(newConnectionString, options =>
|
||||
{
|
||||
if (_options.CommandTimeout > 0)
|
||||
options.CommandTimeout(_options.CommandTimeout);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException($"Database type {_options.DatabaseType} is not supported");
|
||||
}
|
||||
|
||||
// Disponi il vecchio contesto e crea quello nuovo
|
||||
_context.Dispose();
|
||||
_context = new ExistingDatabaseContext(
|
||||
optionsBuilder.Options,
|
||||
_options.ModelConfigurator,
|
||||
_options.EnableAutoDiscovery,
|
||||
_options.EntityAssembly,
|
||||
_options.EntityNamespace,
|
||||
_options.NamingStrategy);
|
||||
|
||||
// Testa la connessione al nuovo database
|
||||
await _context.Database.OpenConnectionAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Errore nel cambio database a '{databaseName}': {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Estrae la connection string del server (senza database specifico) da una connection string completa
|
||||
/// </summary>
|
||||
private string GetServerConnectionString(string connectionString)
|
||||
{
|
||||
var builder = new SqlConnectionStringBuilder(connectionString);
|
||||
builder.InitialCatalog = ""; // Rimuove il database specifico
|
||||
return builder.ConnectionString;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggiorna la connection string con un nuovo database
|
||||
/// </summary>
|
||||
private string UpdateConnectionStringDatabase(string connectionString, string databaseName)
|
||||
{
|
||||
var builder = new SqlConnectionStringBuilder(connectionString);
|
||||
builder.InitialCatalog = databaseName;
|
||||
return builder.ConnectionString;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Crea una connessione database appropriata in base al tipo di database
|
||||
/// </summary>
|
||||
@@ -222,4 +320,58 @@ public class EFCoreDatabaseManager : IDatabaseManager
|
||||
{
|
||||
_context?.Dispose();
|
||||
}
|
||||
|
||||
public async Task<string?> GetPrimaryKeyFieldAsync(string tableName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var connectionString = _context.Database.GetConnectionString();
|
||||
if (connectionString == null)
|
||||
throw new InvalidOperationException("Connection string is null");
|
||||
|
||||
using var connection = CreateConnection(connectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
using var command = connection.CreateCommand();
|
||||
|
||||
// Query per ottenere la Primary Key della tabella
|
||||
// Gestisce anche tabelle con schema (es. "dbo.TableName")
|
||||
string schemaName = "dbo"; // Default schema
|
||||
string tableNameOnly = tableName;
|
||||
|
||||
if (tableName.Contains('.'))
|
||||
{
|
||||
var parts = tableName.Split('.');
|
||||
schemaName = parts[0];
|
||||
tableNameOnly = parts[1];
|
||||
}
|
||||
|
||||
command.CommandText = @"
|
||||
SELECT COLUMN_NAME
|
||||
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
||||
WHERE OBJECTPROPERTY(OBJECT_ID(CONSTRAINT_SCHEMA + '.' + QUOTENAME(CONSTRAINT_NAME)), 'IsPrimaryKey') = 1
|
||||
AND TABLE_SCHEMA = @schemaName
|
||||
AND TABLE_NAME = @tableName
|
||||
ORDER BY ORDINAL_POSITION";
|
||||
|
||||
// Usa parametri per evitare SQL injection
|
||||
var schemaParam = command.CreateParameter();
|
||||
schemaParam.ParameterName = "@schemaName";
|
||||
schemaParam.Value = schemaName;
|
||||
command.Parameters.Add(schemaParam);
|
||||
|
||||
var tableParam = command.CreateParameter();
|
||||
tableParam.ParameterName = "@tableName";
|
||||
tableParam.Value = tableNameOnly;
|
||||
command.Parameters.Add(tableParam);
|
||||
|
||||
var result = await command.ExecuteScalarAsync();
|
||||
return result?.ToString();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Errore nel recupero della Primary Key per la tabella {tableName}: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,13 +17,24 @@ public class SqlServerSchemaProvider : IDatabaseSchemaProvider
|
||||
|
||||
try
|
||||
{
|
||||
Console.WriteLine($"[DEBUG] SqlServerSchemaProvider - Connection string: {connectionString?.Substring(0, Math.Min(50, connectionString?.Length ?? 0))}...");
|
||||
|
||||
using (var connection = new SqlConnection(connectionString))
|
||||
{
|
||||
await connection.OpenAsync();
|
||||
Console.WriteLine($"[DEBUG] SqlServerSchemaProvider - Connessione aperta");
|
||||
|
||||
// Prima verifichiamo se ci sono tabelle utente con una query semplice
|
||||
string testSql = "SELECT COUNT(*) FROM sys.tables WHERE is_ms_shipped = 0";
|
||||
using (var testCommand = new SqlCommand(testSql, connection))
|
||||
{
|
||||
var scalarResult = await testCommand.ExecuteScalarAsync();
|
||||
var tableCount = scalarResult != null ? (int)scalarResult : 0;
|
||||
|
||||
if (tableCount == 0)
|
||||
{
|
||||
return new Dictionary<string, IEnumerable<DbColumnInfo>>(); // Restituisce dizionario vuoto
|
||||
}
|
||||
}
|
||||
|
||||
// Se ci sono tabelle, procediamo con la query completa
|
||||
// Query per ottenere la struttura delle tabelle in SQL Server
|
||||
string sql = @"
|
||||
SELECT
|
||||
@@ -71,8 +82,8 @@ public class SqlServerSchemaProvider : IDatabaseSchemaProvider
|
||||
|
||||
using (var reader = await command.ExecuteReaderAsync())
|
||||
{
|
||||
string currentTable = null;
|
||||
List<DbColumnInfo> columns = null;
|
||||
string? currentTable = null;
|
||||
List<DbColumnInfo>? columns = null;
|
||||
|
||||
while (await reader.ReadAsync())
|
||||
{
|
||||
@@ -117,12 +128,6 @@ public class SqlServerSchemaProvider : IDatabaseSchemaProvider
|
||||
{
|
||||
result[currentTable] = columns;
|
||||
}
|
||||
|
||||
Console.WriteLine($"[DEBUG] SqlServerSchemaProvider - Query completata. Trovate {result.Count} tabelle");
|
||||
foreach (var table in result.Take(3))
|
||||
{
|
||||
Console.WriteLine($"[DEBUG] SqlServerSchemaProvider - Tabella: {table.Key}, Colonne: {table.Value?.Count() ?? 0}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,16 @@ public interface IDatabaseManager : IDisposable
|
||||
/// Esegue un comando SQL che non restituisce risultati
|
||||
/// </summary>
|
||||
Task<int> ExecuteCommandAsync(string sql, params object[] parameters);
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene l'elenco dei database disponibili sul server
|
||||
/// </summary>
|
||||
Task<List<string>> GetAvailableDatabasesAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Cambia il database corrente per la connessione
|
||||
/// </summary>
|
||||
Task ChangeDatabaseAsync(string databaseName);
|
||||
/// <summary>
|
||||
/// Ottiene i metadati delle tabelle nel database
|
||||
/// </summary>
|
||||
@@ -54,6 +64,11 @@ public interface IDatabaseManager : IDisposable
|
||||
/// Ottiene tutti i record da una tabella specifica come dizionari chiave-valore
|
||||
/// </summary>
|
||||
Task<IEnumerable<Dictionary<string, object>>> GetAllRecordsAsync(string tableName);
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene il nome del campo Primary Key di una tabella specifica
|
||||
/// </summary>
|
||||
Task<string?> GetPrimaryKeyFieldAsync(string tableName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -61,11 +76,11 @@ public interface IDatabaseManager : IDisposable
|
||||
/// </summary>
|
||||
public class DbColumnInfo
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string DataType { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string DataType { get; set; } = string.Empty;
|
||||
public bool IsNullable { get; set; }
|
||||
public bool IsPrimaryKey { get; set; }
|
||||
public bool IsForeignKey { get; set; }
|
||||
public string ReferencedTable { get; set; }
|
||||
public string ReferencedColumn { get; set; }
|
||||
public string? ReferencedTable { get; set; }
|
||||
public string? ReferencedColumn { get; set; }
|
||||
}
|
||||
|
||||
@@ -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>>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,9 +42,7 @@ namespace DataConnection.REST.Interfaces
|
||||
/// <param name="entityData">The data for the new entity as key-value pairs.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The created entity data or null if creation failed.</returns>
|
||||
Task<Dictionary<string, object>?> CreateEntityAsync(string entityName, Dictionary<string, object> entityData, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
Task<Dictionary<string, object>?> CreateEntityAsync(string entityName, Dictionary<string, object> entityData, CancellationToken cancellationToken = default); /// <summary>
|
||||
/// Creates a new entity or updates an existing one (upsert operation).
|
||||
/// </summary>
|
||||
/// <param name="entityName">The name of the entity to upsert.</param>
|
||||
@@ -53,6 +51,41 @@ namespace DataConnection.REST.Interfaces
|
||||
/// <returns>The upserted entity data or null if operation failed.</returns>
|
||||
Task<Dictionary<string, object>?> UpsertEntityAsync(string entityName, Dictionary<string, object> entityData, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Searches for entities matching the specified key fields.
|
||||
/// </summary>
|
||||
/// <param name="entityName">The name of the entity to search.</param>
|
||||
/// <param name="keyFields">The key fields and their values to search for.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>A list of matching entities.</returns>
|
||||
Task<List<Dictionary<string, object>>> FindEntitiesByKeysAsync(string entityName, Dictionary<string, object> keyFields, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes an entity by its ID or unique identifier.
|
||||
/// </summary>
|
||||
/// <param name="entityName">The name of the entity to delete.</param>
|
||||
/// <param name="entityId">The ID or unique identifier of the entity to delete.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>True if deletion was successful, false otherwise.</returns>
|
||||
Task<bool> DeleteEntityAsync(string entityName, string entityId, CancellationToken cancellationToken = default); /// <summary>
|
||||
/// Updates an existing entity by its ID with the provided data.
|
||||
/// </summary>
|
||||
/// <param name="entityName">The name of the entity to update.</param>
|
||||
/// <param name="entityId">The ID or unique identifier 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>
|
||||
Task<Dictionary<string, object>?> UpdateEntityAsync(string entityName, string entityId, Dictionary<string, object> entityData, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Searches for entities matching the specified 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>
|
||||
Task<List<Dictionary<string, object>>> FindEntitiesByRequiredFieldsAsync(string entityName, Dictionary<string, object> requiredFields, CancellationToken cancellationToken = default);
|
||||
|
||||
// Add other methods as needed (PUT, DELETE, PATCH, etc.)
|
||||
// Consider adding methods for handling raw HttpResponseMessage or string responses
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user