Files
Data-Coupler/Data_Coupler/Extensions/DataCoupler/RESTMethod.cs
T
Alessio Dal Santo 335d587c89
Build and Push Docker Images / Build Linux Container (push) Successful in 6m56s
Build and Push Docker Images / Build Windows Container (push) Has been cancelled
Build and Push Docker Images / Create Multi-Platform Manifest (push) Has been cancelled
[Feature] Salesforce: batch describe metadati, discovery parallela e fix scheduler External ID
- Salesforce Composite Batch API per describe SObject: le describe sono ora
  raggruppate in chunk da 25 e inviate come singole POST a /composite/batch,
  riducendo le chiamate API da N a ceil(N/25); per 200 SObject: da 201 a 9 chiamate.

- Discovery entita' REST in parallelo: DiscoverEntitySummariesAsync e
  DiscoverEntitiesAsync avviate simultaneamente; la lista entita' diventa
  interattiva subito dopo le summaries, i dettagli completano in background
  con StateHasChanged() per aggiornare l'UI istantaneamente.

- Fix scheduler - preservazione ExternalIdRelationshipsJson e DefaultValuesJson:
  in DataCoupler.razor.cs entrambi i blocchi di update profilo esistente
  (riattivazione profilo inattivo e sovrascrittura profilo attivo) omettevano
  questi campi nella copia, causandone l'azzeramento silenzioso ad ogni
  re-salvataggio. Ora entrambi i percorsi propagano correttamente i campi JSON.

- Fix scheduler - esclusione campi sorgente External ID dal mapping normale:
  in ScheduledProfileExecutionService.TransformRecordForRest i campi sorgente
  usati nelle External ID Relationships venivano inclusi anche nel loop di
  field mapping standard, generando dati duplicati nell'entita' destinazione.
  Ora il comportamento e' allineato alla UI manuale (TransformRecordToRestEntity).

- Aggiornata documentazione: README.md, AGENTS.md, copilot-instructions.md
2026-02-20 14:59:13 +01:00

250 lines
8.7 KiB
C#

using System;
using System.ComponentModel;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.JSInterop;
using Microsoft.Extensions.Logging;
using CredentialManager.Models;
using DataConnection.REST.Interfaces;
using DataConnection.REST.Models;
using DataConnection.CredentialManagement.Interfaces;
using DataConnection.Interfaces;
using Data_Coupler.Services;
using Data_Coupler.Models;
namespace Data_Coupler.Pages;
public partial class DataCoupler : ComponentBase
{
// ===== PROPRIETÀ REST =====
// Credenziali REST
protected List<RestApiCredential> restApiCredentials = new();
protected string selectedRestCredential = "";
// Stato connessioni REST
protected bool isConnectingRest = false;
protected bool isRestConnected = false;
protected string restErrorMessage = "";
// REST discovery
protected List<RestEntitySummary> restEntities = new();
protected RestEntitySummary? selectedRestEntity = null;
protected RestEntityInfo? restEntityDetails = null;
protected string restSearchTerm = "";
// Proprietà di mapping REST
protected string selectedRestProperty = "";
// Servizi REST
protected IRestMetadataDiscovery? currentRestDiscovery = null;
protected IRestServiceClient? currentRestClient = null;
// ===== METODI REST =====
/// <summary>
/// Carica le credenziali REST API
/// </summary>
protected async Task LoadRestCredentials()
{
try
{
restApiCredentials = await CredentialService.GetAllRestApiCredentialsAsync();
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nel caricamento delle credenziali REST");
throw;
}
}
/// <summary>
/// Gestisce il cambio di credenziale REST
/// </summary>
private void OnRestCredentialChanged(ChangeEventArgs e)
{
var newCredential = e.Value?.ToString() ?? "";
// Clear the cache if we're switching to a different credential
if (!string.IsNullOrEmpty(selectedRestCredential) && selectedRestCredential != newCredential)
{
ConnectionFactory.ClearRestClientCache(selectedRestCredential);
Logger.LogInformation("Cleared REST client cache for credential: {CredentialName}", selectedRestCredential);
}
selectedRestCredential = newCredential;
ResetRestState();
}
/// <summary>
/// Resetta lo stato REST
/// </summary>
protected void ResetRestState()
{
isRestConnected = false;
restEntities.Clear();
selectedRestEntity = null;
restEntityDetails = null;
restSearchTerm = "";
restErrorMessage = "";
currentRestDiscovery = null;
currentRestClient = null;
// Clear mappings when resetting REST state - handled by main class
// ClearAllMappings();
}
/// <summary>
/// Connette al servizio REST API
/// </summary>
protected async Task ConnectToRestApi()
{
if (string.IsNullOrEmpty(selectedRestCredential))
return;
isConnectingRest = true;
restErrorMessage = "";
try
{
// Trova la credenziale
var credential = restApiCredentials.FirstOrDefault(c => c.Name == selectedRestCredential);
if (credential == null)
{
restErrorMessage = "Credenziale REST API non trovata";
return;
}
// Test della connessione
var (success, message) = await CredentialService.TestRestApiConnectionAsync(credential.Name);
if (!success)
{
restErrorMessage = $"Connessione fallita: {message}";
return;
}
// Crea i client REST usando il factory con le credenziali complete
currentRestClient = await ConnectionFactory.CreateRestServiceClientAsync(selectedRestCredential);
currentRestDiscovery = await ConnectionFactory.CreateRestMetadataDiscoveryAsync(selectedRestCredential);
Logger.LogInformation("Iniziando autenticazione per il servizio REST {ServiceType} con credenziale: {CredentialName}", credential.ServiceType, selectedRestCredential);
// Autenticazione prima del discovery
var authResult = await currentRestClient.AuthenticateAsync();
if (!authResult)
{
Logger.LogWarning("Autenticazione fallita per il servizio REST {ServiceType}", credential.ServiceType);
restErrorMessage = "Autenticazione fallita per il servizio REST";
return;
}
Logger.LogInformation("Autenticazione completata con successo per il servizio REST {ServiceType}", credential.ServiceType);
// Avvia entrambe le discovery in parallelo:
// - DiscoverEntitySummariesAsync è veloce (1 API call) → sblocca la UI subito
// - DiscoverEntitiesAsync è pesante (batch describe) → completa in background
Logger.LogInformation("Avvio discovery parallela: entity summaries + entity details (batch)...");
var summariesTask = currentRestDiscovery.DiscoverEntitySummariesAsync();
var entitiesTask = currentRestDiscovery.DiscoverEntitiesAsync();
// Attendi le summaries (veloci) e rendi la UI interattiva immediatamente
restEntities = await summariesTask;
isRestConnected = true;
StateHasChanged();
Logger.LogInformation("Entity summaries completate: {EntityCount} entità. UI interattiva.", restEntities.Count);
// Attendi i dettagli completi (già in esecuzione in parallelo)
try
{
availableRelationshipObjects = await entitiesTask;
Logger.LogInformation("Entity details (batch) completati: {Count} oggetti disponibili per External ID Relationships.", availableRelationshipObjects.Count);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Impossibile completare il caricamento dei dettagli entità per External ID Relationships");
availableRelationshipObjects = new List<RestEntityInfo>();
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nella connessione al servizio REST {ServiceType}", selectedRestCredential);
restErrorMessage = $"Errore nella connessione: {ex.Message}";
}
finally
{
isConnectingRest = false;
StateHasChanged();
}
}
/// <summary>
/// Seleziona un'entità REST
/// </summary>
protected async Task SelectRestEntity(RestEntitySummary entity)
{
selectedRestEntity = entity;
// Clear mappings when changing entity - handled by main class
// ClearAllMappings();
try
{
if (currentRestDiscovery != null)
{
// Discovery dei dettagli dell'entità
restEntityDetails = await currentRestDiscovery.DiscoverEntityDetailsAsync(entity.Name);
}
else
{
restErrorMessage = "Servizio di discovery REST non disponibile";
return;
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Errore nel caricamento dettagli entità {EntityName}", entity.Name);
restErrorMessage = $"Errore nel caricamento dettagli entità: {ex.Message}";
}
}
/// <summary>
/// Filtra le entità REST in base al termine di ricerca
/// </summary>
private IEnumerable<RestEntitySummary> GetFilteredRestEntities()
{
if (string.IsNullOrEmpty(restSearchTerm))
return restEntities;
return restEntities.Where(entity =>
entity.Name.Contains(restSearchTerm, StringComparison.OrdinalIgnoreCase) ||
(!string.IsNullOrEmpty(entity.Label) && entity.Label.Contains(restSearchTerm, StringComparison.OrdinalIgnoreCase)));
}
/// <summary>
/// Applica il filtro alle entità REST
/// </summary>
private async Task FilterRestEntities(ChangeEventArgs e)
{
restSearchTerm = e.Value?.ToString() ?? "";
await InvokeAsync(StateHasChanged);
}
/// <summary>
/// Pulisce la ricerca delle entità REST
/// </summary>
private async Task ClearRestSearch()
{
restSearchTerm = "";
await InvokeAsync(StateHasChanged);
}
/// <summary>
/// Seleziona una proprietà REST per il mapping
/// </summary>
protected void SelectRestProperty(string propertyName)
{
selectedRestProperty = propertyName;
}
}