diff --git a/DataConnection/REST/Implementations/BaseRestServiceClient.cs b/DataConnection/REST/Implementations/BaseRestServiceClient.cs index a4447da..79b5c73 100644 --- a/DataConnection/REST/Implementations/BaseRestServiceClient.cs +++ b/DataConnection/REST/Implementations/BaseRestServiceClient.cs @@ -1,10 +1,12 @@ using DataConnection.REST.Configuration; using DataConnection.REST.Interfaces; using System; +using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -98,6 +100,35 @@ namespace DataConnection.REST.Implementations } } + public virtual async Task?> CreateEntityAsync(string entityName, Dictionary entityData, CancellationToken cancellationToken = default) + { + // Default implementation - derived classes should override this for service-specific logic + try + { + var response = await _httpClient.PostAsJsonAsync($"/{entityName}", entityData, cancellationToken); + response.EnsureSuccessStatusCode(); + var responseContent = await response.Content.ReadAsStringAsync(cancellationToken); + if (string.IsNullOrEmpty(responseContent)) + return entityData; // Return original data if no response content + + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + return JsonSerializer.Deserialize>(responseContent, options); + } + catch (HttpRequestException ex) + { + Console.WriteLine($"HTTP Request Error during entity creation: {ex.Message}"); + throw; + } + catch (Exception ex) + { + Console.WriteLine($"Error during entity creation: {ex.Message}"); + throw; + } + } + // Implement other methods (PUT, DELETE, etc.) similarly public void Dispose() diff --git a/DataConnection/REST/Implementations/SalesforceServiceClient.cs b/DataConnection/REST/Implementations/SalesforceServiceClient.cs new file mode 100644 index 0000000..2b5439a --- /dev/null +++ b/DataConnection/REST/Implementations/SalesforceServiceClient.cs @@ -0,0 +1,509 @@ +using DataConnection.REST.Configuration; +using DataConnection.REST.Interfaces; +using DataConnection.REST.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; + +namespace DataConnection.REST.Implementations +{ + /// + /// Client specific for Salesforce REST API. + /// Handles OAuth2 authentication and metadata discovery. + /// + public class SalesforceServiceClient : BaseRestServiceClient, IRestMetadataDiscovery + { + private string? _accessToken; + private string? _instanceUrl; + private DateTime _tokenExpiry; + + public SalesforceServiceClient(HttpClient httpClient, RestServiceOptions options) + : base(httpClient, options) + { + } + + /// + /// Authenticates with Salesforce using Username/Password OAuth2 flow. + /// + /// Connected App Consumer Key + /// Connected App Consumer Secret + /// Salesforce username + /// Salesforce password + security token + /// Cancellation token + /// True if authentication is successful + public async Task AuthenticateAsync(string clientId, string clientSecret, string username, string password, CancellationToken cancellationToken = default) + { + try + { + var tokenEndpoint = "/services/oauth2/token"; + + var tokenRequest = new List> + { + new("grant_type", "password"), + new("client_id", clientId), + new("client_secret", clientSecret), + new("username", username), + new("password", password) + }; + + var formContent = new FormUrlEncodedContent(tokenRequest); + + Console.WriteLine($"--- Salesforce Authentication Attempt ---"); + Console.WriteLine($"Target URL: {_httpClient.BaseAddress}{tokenEndpoint}"); + Console.WriteLine($"Username: {username}"); + Console.WriteLine($"--- End Salesforce Authentication Attempt ---"); + + var response = await _httpClient.PostAsync(tokenEndpoint, formContent, cancellationToken); + + if (!response.IsSuccessStatusCode) + { + var errorContent = await response.Content.ReadAsStringAsync(cancellationToken); + Console.WriteLine($"Salesforce Authentication failed: {response.StatusCode}"); + Console.WriteLine($"Error details: {errorContent}"); + return false; + } + + var tokenResponse = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); + if (tokenResponse != null && !string.IsNullOrEmpty(tokenResponse.AccessToken)) + { + _accessToken = tokenResponse.AccessToken; + _instanceUrl = tokenResponse.InstanceUrl; + _tokenExpiry = DateTime.UtcNow.AddSeconds(3600); // Default 1 hour, Salesforce doesn't always return expires_in + + // Don't change BaseAddress - we'll use absolute URLs for API calls + // Store the instance URL for building complete URLs later + + // Add Authorization header for future requests + _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _accessToken); + + Console.WriteLine($"Salesforce Authentication successful. Token expires around: {_tokenExpiry.ToLocalTime()}"); + Console.WriteLine($"Instance URL: {_instanceUrl}"); + return true; + } + + Console.WriteLine("Salesforce Authentication response could not be parsed."); + return false; + } + catch (HttpRequestException ex) + { + Console.WriteLine($"HTTP Request Error during Salesforce Authentication: {ex.Message}"); + if (ex.InnerException != null) + { + Console.WriteLine($"Inner Exception: {ex.InnerException.Message}"); + } + return false; + } + catch (JsonException ex) + { + Console.WriteLine($"JSON Parsing Error during Salesforce Authentication: {ex.Message}"); + return false; + } + catch (Exception ex) + { + Console.WriteLine($"Error during Salesforce Authentication: {ex.Message}"); + return false; + } + } + + /// + /// Checks if the current access token is active. + /// + public bool IsAuthenticated() + { + return !string.IsNullOrEmpty(_accessToken) && DateTime.UtcNow < _tokenExpiry; + } + + /// + /// Discovers SObjects (entities) and their fields from Salesforce. + /// + /// Cancellation token + /// A list of discovered entity information + public async Task> DiscoverEntitiesAsync(CancellationToken cancellationToken = default) + { + if (!IsAuthenticated()) + { + Console.WriteLine("Error: Not authenticated to Salesforce. Cannot discover metadata."); + return new List(); + } + + var entities = new List(); + try + { + // First, get list of all SObjects + var sobjectsEndpoint = $"{_instanceUrl}/services/data/v60.0/sobjects/"; + var response = await _httpClient.GetAsync(sobjectsEndpoint, cancellationToken); + response.EnsureSuccessStatusCode(); + + var sobjectsResponse = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); if (sobjectsResponse?.SObjects != null) + { + // For demo purposes, limit to first 20 objects to avoid too many API calls + var limitedSObjects = sobjectsResponse.SObjects.ToList(); + + // Process SObjects in parallel for better performance + var semaphore = new SemaphoreSlim(20, 20); // Limit concurrent requests to 5 + var tasks = limitedSObjects.Where(sobject => !string.IsNullOrEmpty(sobject.Name)) + .Select(async sobject => + { + await semaphore.WaitAsync(cancellationToken); + try + { + // Get detailed field information for each SObject + var describeEndpoint = $"{_instanceUrl}/services/data/v60.0/sobjects/{sobject.Name}/describe/"; + var describeResponse = await _httpClient.GetAsync(describeEndpoint, cancellationToken); + + if (describeResponse.IsSuccessStatusCode) + { + var describeResult = await describeResponse.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); + + if (describeResult?.Fields != null) + { + var entityInfo = new RestEntityInfo + { + Name = sobject.Name + }; + + foreach (var field in describeResult.Fields) + { + if (string.IsNullOrEmpty(field.Name)) continue; + + var propInfo = new RestPropertyInfo + { + Name = field.Name, + Type = field.Type ?? "string", + IsKey = field.Name.Equals("Id", StringComparison.OrdinalIgnoreCase) + }; + entityInfo.Properties.Add(propInfo); + } + + return entityInfo; + } + } + return null; + } + catch (Exception ex) + { + Console.WriteLine($"Error describing SObject {sobject.Name}: {ex.Message}"); + return null; + } + finally + { + semaphore.Release(); + } + }); + + var results = await Task.WhenAll(tasks); + entities.AddRange(results.Where(result => result != null)!); + } + } + catch (HttpRequestException ex) + { + Console.WriteLine($"HTTP Request Error during Salesforce metadata discovery: {ex.Message}"); + } + catch (JsonException ex) + { + Console.WriteLine($"JSON Parsing Error during Salesforce metadata discovery: {ex.Message}"); + } + catch (Exception ex) + { + Console.WriteLine($"Error during Salesforce metadata discovery: {ex.Message}"); + } + + return entities; + } + + /// + /// Discovers a list of available SObjects from Salesforce without detailed field information. + /// + /// Cancellation token + /// A list of discovered SObject summaries + public async Task> DiscoverEntitySummariesAsync(CancellationToken cancellationToken = default) + { + if (!IsAuthenticated()) + { + Console.WriteLine("Error: Not authenticated to Salesforce. Cannot discover metadata."); + return new List(); + } + + var entities = new List(); + try + { + // Get list of all SObjects + var sobjectsEndpoint = $"{_instanceUrl}/services/data/v60.0/sobjects/"; + var response = await _httpClient.GetAsync(sobjectsEndpoint, cancellationToken); + response.EnsureSuccessStatusCode(); + + var sobjectsResponse = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); + + if (sobjectsResponse?.SObjects != null) + { + foreach (var sobject in sobjectsResponse.SObjects) + { + if (string.IsNullOrEmpty(sobject.Name)) continue; + + var entitySummary = new RestEntitySummary + { + Name = sobject.Name, + Label = sobject.Label ?? sobject.Name, + IsCustom = sobject.Custom, + EntityType = "SObject" + }; + entities.Add(entitySummary); + } + } + } + catch (HttpRequestException ex) + { + Console.WriteLine($"HTTP Request Error during Salesforce metadata discovery: {ex.Message}"); + } + catch (JsonException ex) + { + Console.WriteLine($"JSON Parsing Error during Salesforce metadata discovery: {ex.Message}"); + } + catch (Exception ex) + { + Console.WriteLine($"Error during Salesforce metadata discovery: {ex.Message}"); + } + + return entities.OrderBy(e => e.Name).ToList(); + } + + /// + /// Discovers detailed information for a specific SObject including all its fields. + /// + /// The name of the SObject to get details for + /// Cancellation token + /// Detailed SObject information or null if not found + public async Task DiscoverEntityDetailsAsync(string entityName, CancellationToken cancellationToken = default) + { + if (!IsAuthenticated()) + { + Console.WriteLine("Error: Not authenticated to Salesforce. Cannot discover entity details."); + return null; + } + + try + { + // Get detailed field information for the specific SObject + var describeEndpoint = $"{_instanceUrl}/services/data/v60.0/sobjects/{entityName}/describe/"; + var describeResponse = await _httpClient.GetAsync(describeEndpoint, cancellationToken); + + if (!describeResponse.IsSuccessStatusCode) + { + Console.WriteLine($"Failed to get details for SObject {entityName}: {describeResponse.StatusCode}"); + return null; + } + + var describeResult = await describeResponse.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); + + if (describeResult?.Fields != null) + { + var entityInfo = new RestEntityInfo + { + Name = entityName + }; + + foreach (var field in describeResult.Fields) + { + if (string.IsNullOrEmpty(field.Name)) continue; + + var propInfo = new RestPropertyInfo + { + Name = field.Name, + Type = field.Type ?? "string", + IsKey = field.Name.Equals("Id", StringComparison.OrdinalIgnoreCase), + IsRequired = !field.Nillable, + MaxLength = field.Length > 0 ? field.Length : null + }; + entityInfo.Properties.Add(propInfo); + } + + return entityInfo; + } + } + catch (HttpRequestException ex) + { + Console.WriteLine($"HTTP Request Error during Salesforce entity details discovery: {ex.Message}"); + } + catch (JsonException ex) + { + Console.WriteLine($"JSON Parsing Error during Salesforce entity details discovery: {ex.Message}"); + } + catch (Exception ex) + { + Console.WriteLine($"Error during Salesforce entity details discovery: {ex.Message}"); + } + + return null; + } + + /// + /// Creates a new SObject in Salesforce. + /// + /// The name of the SObject to create (e.g., "Account", "Contact"). + /// The data for the new SObject as key-value pairs. + /// Cancellation token. + /// The created entity data or null if creation failed. + public override async Task?> CreateEntityAsync(string entityName, Dictionary entityData, CancellationToken cancellationToken = default) + { + if (!IsAuthenticated()) + { + Console.WriteLine("Error: Not authenticated to Salesforce. Cannot create entity."); + return null; + } // Salesforce REST API endpoint for creating SObjects + var createUri = $"{_instanceUrl}/services/data/v60.0/sobjects/{entityName}/"; + + try + { + Console.WriteLine($"--- Salesforce Entity Creation Attempt ---"); + Console.WriteLine($"SObject: {entityName}"); + Console.WriteLine($"Target URL: {createUri}"); + Console.WriteLine($"Data: {System.Text.Json.JsonSerializer.Serialize(entityData)}"); + + var response = await _httpClient.PostAsJsonAsync(createUri, entityData, cancellationToken); + + if (!response.IsSuccessStatusCode) + { + var errorContent = await response.Content.ReadAsStringAsync(cancellationToken); + Console.WriteLine($"Salesforce Entity Creation failed: {response.StatusCode}"); + Console.WriteLine($"Error details: {errorContent}"); + Console.WriteLine($"--- End Salesforce Entity Creation Attempt (Failed) ---"); + return null; + } + + var responseContent = await response.Content.ReadAsStringAsync(cancellationToken); + Console.WriteLine($"Salesforce Entity Creation successful"); + Console.WriteLine($"Response: {responseContent}"); + Console.WriteLine($"--- End Salesforce Entity Creation Attempt (Success) ---"); + + if (string.IsNullOrEmpty(responseContent)) + return entityData; // Return original data if no response content + + // Salesforce returns creation result with Id and success status + var creationResult = System.Text.Json.JsonSerializer.Deserialize>(responseContent); + + // Merge the original data with the creation result (which includes the new Id) + if (creationResult != null) + { + var result = new Dictionary(entityData); + foreach (var kvp in creationResult) + { + result[kvp.Key] = kvp.Value; + } + return result; + } + + return creationResult; + } + catch (HttpRequestException ex) + { + Console.WriteLine($"HTTP Request Error during Salesforce entity creation: {ex.Message}"); + if (ex.InnerException != null) + { + Console.WriteLine($"Inner Exception: {ex.InnerException.Message}"); + } + Console.WriteLine($"--- End Salesforce Entity Creation Attempt (Exception) ---"); + return null; + } + catch (JsonException ex) + { + Console.WriteLine($"JSON Parsing Error during Salesforce entity creation: {ex.Message}"); + Console.WriteLine($"--- End Salesforce Entity Creation Attempt (JsonException) ---"); + return null; + } + catch (Exception ex) + { + Console.WriteLine($"Error during Salesforce entity creation: {ex.Message}"); + Console.WriteLine($"--- End Salesforce Entity Creation Attempt (Exception) ---"); + return null; + } + } + + // --- Nested classes for deserializing Salesforce responses --- + private class SalesforceTokenResponse + { + [JsonPropertyName("access_token")] + public string AccessToken { get; set; } = string.Empty; + + [JsonPropertyName("instance_url")] + public string InstanceUrl { get; set; } = string.Empty; + + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + [JsonPropertyName("token_type")] + public string TokenType { get; set; } = string.Empty; + + [JsonPropertyName("issued_at")] + public string IssuedAt { get; set; } = string.Empty; + + [JsonPropertyName("signature")] + public string Signature { get; set; } = string.Empty; + } + + private class SalesforceSObjectsResponse + { + [JsonPropertyName("encoding")] + public string Encoding { get; set; } = string.Empty; + + [JsonPropertyName("maxBatchSize")] + public int MaxBatchSize { get; set; } + + [JsonPropertyName("sobjects")] + public List SObjects { get; set; } = new List(); + } + + private class SalesforceSObject + { + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("label")] + public string Label { get; set; } = string.Empty; + + [JsonPropertyName("custom")] + public bool Custom { get; set; } + + [JsonPropertyName("keyPrefix")] + public string KeyPrefix { get; set; } = string.Empty; + } + + private class SalesforceDescribeResponse + { + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("label")] + public string Label { get; set; } = string.Empty; + + [JsonPropertyName("fields")] + public List Fields { get; set; } = new List(); + } + + private class SalesforceField + { + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("type")] + public string Type { get; set; } = string.Empty; + + [JsonPropertyName("label")] + public string Label { get; set; } = string.Empty; + + [JsonPropertyName("length")] + public int Length { get; set; } + + [JsonPropertyName("nillable")] + public bool Nillable { get; set; } + + [JsonPropertyName("unique")] + public bool Unique { get; set; } + } + } +} \ No newline at end of file diff --git a/DataConnection/REST/Implementations/SapB1ServiceClient.cs b/DataConnection/REST/Implementations/SapB1ServiceClient.cs index 4ea9466..1d79b2d 100644 --- a/DataConnection/REST/Implementations/SapB1ServiceClient.cs +++ b/DataConnection/REST/Implementations/SapB1ServiceClient.cs @@ -1,6 +1,7 @@ using DataConnection.REST.Configuration; using DataConnection.REST.Interfaces; using DataConnection.REST.Models; // Added for metadata models +using Microsoft.EntityFrameworkCore.Storage.Json; using System; using System.Collections.Generic; using System.Linq; @@ -9,6 +10,7 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using System.Xml; @@ -79,11 +81,19 @@ namespace DataConnection.REST.Implementations public async Task LoginAsync(string companyDB, string userName, string password, CancellationToken cancellationToken = default) { var loginPayload = new { CompanyDB = companyDB, UserName = userName, Password = password }; - var loginUri = "/b1s/v2/Login"; // Assuming v1, adjust if needed + var loginUri = "/b1s/v2/Login"; // Assuming v2, adjust if needed // --- Logging Aggiunto --- + var absoluteUri = _sapHttpClient.BaseAddress != null ? new Uri(_sapHttpClient.BaseAddress, loginUri).ToString() : loginUri; + Console.WriteLine($"--- SAP B1 Login Attempt ---"); + Console.WriteLine($"Target URL: {absoluteUri}"); + Console.WriteLine($"Payload: CompanyDB='{companyDB}', UserName='{userName}', Password='{(string.IsNullOrEmpty(password) ? "" : password.Substring(0, Math.Min(1, password.Length)) + "...")}'"); // Non loggare la password completa in produzione! + // Verifica se IgnoreSslErrors è attivo (preso dalle opzioni passate al costruttore) + Console.WriteLine($"Ignore SSL Errors: {_options.IgnoreSslErrors}"); + // --- Fine Logging --- try { // Use the internal HttpClient configured with CookieContainer + // Pass the loginPayload object directly, PostAsJsonAsync handles serialization var response = await _sapHttpClient.PostAsJsonAsync(loginUri, loginPayload, cancellationToken); if (!response.IsSuccessStatusCode) @@ -92,6 +102,7 @@ namespace DataConnection.REST.Implementations Console.WriteLine($"SAP B1 Login failed: {response.StatusCode}"); var errorContent = await response.Content.ReadAsStringAsync(cancellationToken); Console.WriteLine($"Error details: {errorContent}"); + Console.WriteLine($"--- End SAP B1 Login Attempt (Failed) ---"); // Log fine tentativo return false; } @@ -107,6 +118,7 @@ namespace DataConnection.REST.Implementations _sessionTimeout = DateTime.UtcNow.AddSeconds(sessionInfo.SessionTimeout); Console.WriteLine($"SAP B1 Login successful. Session expires around: {_sessionTimeout.ToLocalTime()}"); + Console.WriteLine($"--- End SAP B1 Login Attempt (Success) ---"); // Log fine tentativo return true; } @@ -116,18 +128,30 @@ namespace DataConnection.REST.Implementations catch (HttpRequestException ex) { Console.WriteLine($"HTTP Request Error during SAP B1 Login: {ex.Message}"); + // Logga dettagli sull'eccezione interna se presente (spesso utile per problemi SSL) + if (ex.InnerException != null) + { + Console.WriteLine($"Inner Exception: {ex.InnerException.Message}"); + } + Console.WriteLine($"--- End SAP B1 Login Attempt (Exception) ---"); // Log fine tentativo return false; } catch (JsonException ex) { Console.WriteLine($"JSON Parsing Error during SAP B1 Login: {ex.Message}"); + Console.WriteLine($"--- End SAP B1 Login Attempt (JsonException) ---"); // Log fine tentativo return false; } catch (Exception ex) { Console.WriteLine($"Error during SAP B1 Login: {ex.Message}"); + Console.WriteLine($"--- End SAP B1 Login Attempt (Exception) ---"); // Log fine tentativo return false; } + // finally // Aggiunto per sicurezza - Rimosso perchè non necessario qui + // { + // // Potrebbe essere utile loggare qui se necessario + // } } /// @@ -276,6 +300,248 @@ namespace DataConnection.REST.Implementations } + /// + /// Creates a new entity in SAP Business One using the Service Layer. + /// + /// The name of the entity to create (e.g., "Items", "BusinessPartners"). + /// The data for the new entity as key-value pairs. + /// Cancellation token. + /// The created entity data or null if creation failed. + public override async Task?> CreateEntityAsync(string entityName, Dictionary entityData, CancellationToken cancellationToken = default) + { + // Ensure session is active before attempting creation + if (!IsSessionActive()) + { + Console.WriteLine("Error: Not logged into SAP B1 Service Layer. Cannot create entity."); + return null; + } + + // SAP B1 Service Layer typically uses plural entity names for endpoints + var createUri = $"/b1s/v2/{entityName}"; + + try + { Console.WriteLine($"--- SAP B1 Entity Creation Attempt ---"); + Console.WriteLine($"Entity: {entityName}"); + Console.WriteLine($"Target URL: {(_sapHttpClient.BaseAddress != null ? new Uri(_sapHttpClient.BaseAddress, createUri) : createUri)}"); + Console.WriteLine($"Data: {System.Text.Json.JsonSerializer.Serialize(entityData)}"); + + var response = await _sapHttpClient.PostAsJsonAsync(createUri, entityData, cancellationToken); + + if (!response.IsSuccessStatusCode) + { + var errorContent = await response.Content.ReadAsStringAsync(cancellationToken); + Console.WriteLine($"SAP B1 Entity Creation failed: {response.StatusCode}"); + Console.WriteLine($"Error details: {errorContent}"); + Console.WriteLine($"--- End SAP B1 Entity Creation Attempt (Failed) ---"); + return null; + } + + var responseContent = await response.Content.ReadAsStringAsync(cancellationToken); + Console.WriteLine($"SAP B1 Entity Creation successful"); + Console.WriteLine($"Response: {responseContent}"); + Console.WriteLine($"--- End SAP B1 Entity Creation Attempt (Success) ---"); + + if (string.IsNullOrEmpty(responseContent)) + return entityData; // Return original data if no response content + + return System.Text.Json.JsonSerializer.Deserialize>(responseContent); + } + catch (HttpRequestException ex) + { + Console.WriteLine($"HTTP Request Error during SAP B1 entity creation: {ex.Message}"); + if (ex.InnerException != null) + { + Console.WriteLine($"Inner Exception: {ex.InnerException.Message}"); + } + Console.WriteLine($"--- End SAP B1 Entity Creation Attempt (Exception) ---"); + return null; + } + catch (JsonException ex) + { + Console.WriteLine($"JSON Parsing Error during SAP B1 entity creation: {ex.Message}"); + Console.WriteLine($"--- End SAP B1 Entity Creation Attempt (JsonException) ---"); + return null; + } + catch (Exception ex) + { + Console.WriteLine($"Error during SAP B1 entity creation: {ex.Message}"); + Console.WriteLine($"--- End SAP B1 Entity Creation Attempt (Exception) ---"); + return null; + } + } + + /// + /// Discovers a list of available entities from SAP B1 Service Layer without detailed field information. + /// + /// Cancellation token + /// A list of discovered entity summaries + public async Task> DiscoverEntitySummariesAsync(CancellationToken cancellationToken = default) + { + if (!IsSessionActive()) + { + Console.WriteLine("Error: Not logged into SAP B1 Service Layer. Cannot discover metadata."); + return new List(); + } + + var entities = new List(); + var metadataUri = "/b1s/v2/$metadata"; + + try + { + var response = await _sapHttpClient.GetAsync(metadataUri, cancellationToken); + response.EnsureSuccessStatusCode(); + + var xmlContent = await response.Content.ReadAsStringAsync(cancellationToken); + var edmx = XDocument.Parse(xmlContent); + + // Define XML namespaces + XNamespace edm = "http://schemas.microsoft.com/ado/2009/11/edm"; + XNamespace edmxNs = "http://schemas.microsoft.com/ado/2007/06/edmx"; + XNamespace defaultEdmNs = edmx.Root?.GetDefaultNamespace() ?? edm; + XNamespace dataServicesNs = edmx.Root?.Element(edmxNs + "DataServices")?.GetDefaultNamespace() ?? defaultEdmNs; + + var schemaElement = edmx.Descendants(defaultEdmNs + "Schema").FirstOrDefault() ?? + edmx.Descendants(dataServicesNs + "Schema").FirstOrDefault(); + + if (schemaElement == null) + { + Console.WriteLine("Error: Could not find Schema element in $metadata response."); + return entities; + } + + // Find all EntityType elements within the Schema + foreach (var entityTypeElement in schemaElement.Elements(defaultEdmNs + "EntityType")) + { + var entityName = entityTypeElement.Attribute("Name")?.Value; + if (string.IsNullOrEmpty(entityName)) continue; + + var entitySummary = new RestEntitySummary + { + Name = entityName, + Label = entityName, // SAP B1 typically doesn't have separate labels + IsCustom = entityName.StartsWith("U_", StringComparison.OrdinalIgnoreCase), + EntityType = "EntityType" + }; + entities.Add(entitySummary); + } + } + catch (HttpRequestException ex) + { + Console.WriteLine($"HTTP Request Error during SAP B1 metadata discovery: {ex.Message}"); + } + catch (XmlException ex) + { + Console.WriteLine($"XML Parsing Error during SAP B1 metadata discovery: {ex.Message}"); + } + catch (Exception ex) + { + Console.WriteLine($"Error during SAP B1 metadata discovery: {ex.Message}"); + } + + return entities.OrderBy(e => e.Name).ToList(); + } + + /// + /// Discovers detailed information for a specific entity including all its properties. + /// + /// The name of the entity to get details for + /// Cancellation token + /// Detailed entity information or null if not found + public async Task DiscoverEntityDetailsAsync(string entityName, CancellationToken cancellationToken = default) + { + if (!IsSessionActive()) + { + Console.WriteLine("Error: Not logged into SAP B1 Service Layer. Cannot discover entity details."); + return null; + } + + var metadataUri = "/b1s/v2/$metadata"; + + try + { + var response = await _sapHttpClient.GetAsync(metadataUri, cancellationToken); + response.EnsureSuccessStatusCode(); + + var xmlContent = await response.Content.ReadAsStringAsync(cancellationToken); + var edmx = XDocument.Parse(xmlContent); + + // Define XML namespaces + XNamespace edm = "http://schemas.microsoft.com/ado/2009/11/edm"; + XNamespace edmxNs = "http://schemas.microsoft.com/ado/2007/06/edmx"; + XNamespace defaultEdmNs = edmx.Root?.GetDefaultNamespace() ?? edm; + XNamespace dataServicesNs = edmx.Root?.Element(edmxNs + "DataServices")?.GetDefaultNamespace() ?? defaultEdmNs; + + var schemaElement = edmx.Descendants(defaultEdmNs + "Schema").FirstOrDefault() ?? + edmx.Descendants(dataServicesNs + "Schema").FirstOrDefault(); + + if (schemaElement == null) + { + Console.WriteLine("Error: Could not find Schema element in $metadata response."); + return null; + } + + // Find the specific EntityType element + var entityTypeElement = schemaElement.Elements(defaultEdmNs + "EntityType") + .FirstOrDefault(e => e.Attribute("Name")?.Value == entityName); + + if (entityTypeElement == null) + { + Console.WriteLine($"Error: Could not find EntityType '{entityName}' in $metadata response."); + return null; + } + + var entityInfo = new RestEntityInfo + { + Name = entityName + }; + + // Find key properties for this entity type + var keyProperties = new HashSet(); + var keyElement = entityTypeElement.Element(defaultEdmNs + "Key"); + if (keyElement != null) + { + foreach (var propRef in keyElement.Elements(defaultEdmNs + "PropertyRef")) + { + var keyName = propRef.Attribute("Name")?.Value; + if (!string.IsNullOrEmpty(keyName)) keyProperties.Add(keyName); + } + } + + // Find properties for this entity type + foreach (var propertyElement in entityTypeElement.Elements(defaultEdmNs + "Property")) + { + var propName = propertyElement.Attribute("Name")?.Value; + if (string.IsNullOrEmpty(propName)) continue; + + var propInfo = new RestPropertyInfo + { + Name = propName, + Type = propertyElement.Attribute("Type")?.Value ?? string.Empty, + IsKey = keyProperties.Contains(propName), + IsRequired = propertyElement.Attribute("Nullable")?.Value?.ToLower() == "false", + MaxLength = int.TryParse(propertyElement.Attribute("MaxLength")?.Value, out var maxLen) ? maxLen : null + }; + entityInfo.Properties.Add(propInfo); + } + + return entityInfo; + } + catch (HttpRequestException ex) + { + Console.WriteLine($"HTTP Request Error during SAP B1 entity details discovery: {ex.Message}"); + } + catch (XmlException ex) + { + Console.WriteLine($"XML Parsing Error during SAP B1 entity details discovery: {ex.Message}"); + } + catch (Exception ex) + { + Console.WriteLine($"Error during SAP B1 entity details discovery: {ex.Message}"); + } + + return null; + } + // Helper to get cookie value private string? GetCookieValue(string cookieName) { diff --git a/DataConnection/REST/Interfaces/IRestMetadataDiscovery.cs b/DataConnection/REST/Interfaces/IRestMetadataDiscovery.cs index 60cc17c..3820557 100644 --- a/DataConnection/REST/Interfaces/IRestMetadataDiscovery.cs +++ b/DataConnection/REST/Interfaces/IRestMetadataDiscovery.cs @@ -10,6 +10,21 @@ namespace DataConnection.REST.Interfaces /// public interface IRestMetadataDiscovery { + /// + /// Discovers a list of available entities from the REST service without detailed field information. + /// + /// Cancellation token. + /// A list of discovered entity summaries. + Task> DiscoverEntitySummariesAsync(CancellationToken cancellationToken = default); + + /// + /// Discovers detailed information for a specific entity including all its properties. + /// + /// The name of the entity to get details for. + /// Cancellation token. + /// Detailed entity information or null if not found. + Task DiscoverEntityDetailsAsync(string entityName, CancellationToken cancellationToken = default); + /// /// Discovers entities and their properties from the REST service metadata. /// diff --git a/DataConnection/REST/Interfaces/IRestServiceClient.cs b/DataConnection/REST/Interfaces/IRestServiceClient.cs index 1d5674a..cb11ebd 100644 --- a/DataConnection/REST/Interfaces/IRestServiceClient.cs +++ b/DataConnection/REST/Interfaces/IRestServiceClient.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Threading.Tasks; namespace DataConnection.REST.Interfaces @@ -27,6 +28,15 @@ namespace DataConnection.REST.Interfaces /// The deserialized response content. Task PostAsync(string requestUri, TRequest payload, CancellationToken cancellationToken = default); + /// + /// Creates a new entity by sending a POST request with the provided data. + /// + /// The name of the entity to create. + /// The data for the new entity as key-value pairs. + /// Cancellation token. + /// The created entity data or null if creation failed. + Task?> CreateEntityAsync(string entityName, Dictionary entityData, CancellationToken cancellationToken = default); + // Add other methods as needed (PUT, DELETE, PATCH, etc.) // Consider adding methods for handling raw HttpResponseMessage or string responses } diff --git a/DataConnection/REST/Models/RestMetadataInfo.cs b/DataConnection/REST/Models/RestMetadataInfo.cs index 9c18a00..f35171a 100644 --- a/DataConnection/REST/Models/RestMetadataInfo.cs +++ b/DataConnection/REST/Models/RestMetadataInfo.cs @@ -2,6 +2,18 @@ using System.Collections.Generic; namespace DataConnection.REST.Models { + /// + /// Represents basic information about a discovered REST entity without detailed field information. + /// + public class RestEntitySummary + { + public string Name { get; set; } = string.Empty; + public string Label { get; set; } = string.Empty; + public bool IsCustom { get; set; } = false; + public string Description { get; set; } = string.Empty; + public string EntityType { get; set; } = string.Empty; // For distinguishing between different types of entities + } + /// /// Represents information about a discovered REST entity (resource). /// @@ -20,6 +32,20 @@ namespace DataConnection.REST.Models public string Name { get; set; } = string.Empty; public string Type { get; set; } = string.Empty; // Type as defined in the metadata (e.g., Edm.String, Edm.Int32) public bool IsKey { get; set; } = false; + public bool IsRequired { get; set; } = false; + public bool IsReadOnly { get; set; } = false; + public int? MaxLength { get; set; } // Add other relevant info like Nullable, MaxLength etc. if needed } + + /// + /// Represents the result of a REST entity creation operation. + /// + public class RestEntityCreationResult + { + public bool Success { get; set; } + public string? ErrorMessage { get; set; } + public Dictionary? CreatedEntity { get; set; } + public string? CreatedEntityId { get; set; } + } }