diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa9cabd..fe8f3dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,7 +50,7 @@ jobs: run: dotnet test src/NetCoreForce.Client.Tests --configuration $config --no-restore --no-build --verbosity normal - name: Upload nuget artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: nuget-package path: ${{github.workspace}}/packages/NetCoreForce.*.nupkg diff --git a/src/NetCoreForce.Client/Enumerations/CompositeMethod.cs b/src/NetCoreForce.Client/Enumerations/CompositeMethod.cs index b56f421..b62bef5 100644 --- a/src/NetCoreForce.Client/Enumerations/CompositeMethod.cs +++ b/src/NetCoreForce.Client/Enumerations/CompositeMethod.cs @@ -4,6 +4,8 @@ public enum CompositeMethod { Read, Write, - Delete + Delete, + Create, + Update } } diff --git a/src/NetCoreForce.Client/Enumerations/IngestJobResultType.cs b/src/NetCoreForce.Client/Enumerations/IngestJobResultType.cs new file mode 100644 index 0000000..4f5bf2b --- /dev/null +++ b/src/NetCoreForce.Client/Enumerations/IngestJobResultType.cs @@ -0,0 +1,9 @@ +namespace NetCoreForce.Client.Enumerations +{ + public enum IngestJobResultType + { + Successful, + Failed, + Unprocessed + } +} diff --git a/src/NetCoreForce.Client/Enumerations/JobType.cs b/src/NetCoreForce.Client/Enumerations/JobType.cs new file mode 100644 index 0000000..d802f69 --- /dev/null +++ b/src/NetCoreForce.Client/Enumerations/JobType.cs @@ -0,0 +1,8 @@ +namespace NetCoreForce.Client.Enumerations +{ + public enum JobType + { + Query, + Ingest + } +} diff --git a/src/NetCoreForce.Client/Extensions/UriExtensions.cs b/src/NetCoreForce.Client/Extensions/UriExtensions.cs new file mode 100644 index 0000000..77140be --- /dev/null +++ b/src/NetCoreForce.Client/Extensions/UriExtensions.cs @@ -0,0 +1,17 @@ +using System; +using System.Web; + +namespace NetCoreForce.Client.Extensions +{ + public static class UriExtensions + { + public static Uri AddQueryParameter(this Uri uri, string key, string value) + { + var uriBuilder = new UriBuilder(uri); + var query = HttpUtility.ParseQueryString(uriBuilder.Query); + query[key] = value; + uriBuilder.Query = query.ToString(); + return uriBuilder.Uri; + } + } +} diff --git a/src/NetCoreForce.Client/ForceClient.cs b/src/NetCoreForce.Client/ForceClient.cs index e7a9f38..40fc829 100644 --- a/src/NetCoreForce.Client/ForceClient.cs +++ b/src/NetCoreForce.Client/ForceClient.cs @@ -1,4 +1,5 @@ using NetCoreForce.Client.Enumerations; +using NetCoreForce.Client.Extensions; using NetCoreForce.Client.Models; using System; using System.Collections.Generic; @@ -7,6 +8,7 @@ using System.Linq; using System.Net.Http; using System.Runtime.CompilerServices; +using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -688,9 +690,17 @@ public async Task ExecuteCompositeRecords( List subRequests = sObjects.Select(s => { + var method = s.Method switch + { + CompositeMethod.Write => string.IsNullOrWhiteSpace(s.Id) ? "POST" : "PATCH", + CompositeMethod.Delete => "DELETE", + CompositeMethod.Create => "POST", + CompositeMethod.Update => "PATCH", + _ => "GET", + }; return new CompositeSubRequest( s.SObject, - s.Method == CompositeMethod.Write ? string.IsNullOrWhiteSpace(s.Id) ? "POST" : "PATCH" : s.Method == CompositeMethod.Delete ? "DELETE" : "GET", + method, s.ReferenceId, s.CompositeType == CompositeType.SObject ? UriFormatter.CompositeSubRequest(ApiVersion, s.Type, s.Id) : UriFormatter.CompositeSObjectCollectionsSubRequest(ApiVersion) ); @@ -828,6 +838,182 @@ private async Task BlobRetrieveResponse(string sObjectTypeN throw new ForceApiException($"Failed to download blob data, request returned {responseMessage.StatusCode} {responseMessage.ReasonPhrase}"); } + #region bulk methods + + // BULK METHODS + + public async Task CreateQueryJobAsync(string queryString, bool queryAll = false) + { + if (string.IsNullOrEmpty(queryString)) throw new ArgumentNullException(nameof(queryString)); + + JsonClient client = new JsonClient(AccessToken, SharedHttpClient); + + var jobInfo = new QueryJobInfo + { + Operation = queryAll ? "queryAll" : "query", + Query = queryString + }; + + var uri = UriFormatter.CreateJob(InstanceUrl, ApiVersion, JobType.Query); + + return await client.HttpPostAsync(jobInfo, uri); + } + + public async Task GetQueryJobInfoResultAsync(string jobId) + { + if (string.IsNullOrEmpty(jobId)) throw new ArgumentNullException(nameof(jobId)); + + var uri = UriFormatter.Job(InstanceUrl, ApiVersion, JobType.Query, jobId); + + JsonClient client = new JsonClient(AccessToken, SharedHttpClient); + + return await client.HttpGetAsync(uri); + } + + public async Task> GetQueryJobResultAsync(string jobId, Func> converter, string locator = null, int? maxRecords = null) + { + if (string.IsNullOrEmpty(jobId)) throw new ArgumentNullException(nameof(jobId)); + + var uri = UriFormatter.Job(InstanceUrl, ApiVersion, JobType.Query, jobId, "results"); + + if (!string.IsNullOrEmpty(locator)) + { + uri = uri.AddQueryParameter("locator", locator); + } + + if (maxRecords.HasValue) + { + uri = uri.AddQueryParameter("maxRecords", maxRecords.ToString()); + } + + JsonClient client = new JsonClient(AccessToken, SharedHttpClient); + + var result = await client.HttpGetAsync(uri); + QueryJobResult response; + + response = new QueryJobResult(result) + { + Items = converter(result.Items) + }; + + return response; + } + + public async Task CreateIngestJobAsync(IngestJobInfo jobInfo) + { + if (jobInfo == null) throw new ArgumentNullException(nameof(jobInfo)); + + JsonClient client = new JsonClient(AccessToken, SharedHttpClient); + + var uri = UriFormatter.CreateJob(InstanceUrl, ApiVersion, JobType.Ingest); + + return await client.HttpPostAsync(jobInfo, uri); + } + + public async Task IngestJobAbortAsync(string jobId) + { + if (string.IsNullOrEmpty(jobId)) throw new ArgumentNullException(nameof(jobId)); + + JsonClient client = new JsonClient(AccessToken, SharedHttpClient); + + var jobInfoUpdate = new IngestJobInfoUpdate + { + State = "Aborted" + }; + + var uri = UriFormatter.Job(InstanceUrl, ApiVersion, JobType.Ingest, jobId); + + return await client.HttpPatchAsync(jobInfoUpdate, uri); + } + + public async Task IngestJobDeleteAsync(string jobId) + { + if (string.IsNullOrEmpty(jobId)) throw new ArgumentNullException(nameof(jobId)); + + JsonClient client = new JsonClient(AccessToken, SharedHttpClient); + + var uri = UriFormatter.Job(InstanceUrl, ApiVersion, JobType.Ingest, jobId); + + return await client.HttpDeleteAsync(uri); + } + + public async Task IngestJobUploadAsync(string jobId, string data) + { + if (string.IsNullOrEmpty(jobId)) throw new ArgumentNullException(nameof(jobId)); + if (data == null) throw new ArgumentNullException(nameof(data)); + + JsonClient client = new JsonClient(AccessToken, SharedHttpClient); + + var uri = UriFormatter.Job(InstanceUrl, ApiVersion, JobType.Ingest, jobId, "batches"); + + await client.HttpAsync(uri, HttpMethod.Put, new StringContent(data, Encoding.UTF8, "text/csv"), deserializeResponse: false); + + return true; + } + + public async Task IngestJobUploadCompleteAsync(string jobId) + { + if (string.IsNullOrEmpty(jobId)) throw new ArgumentNullException(nameof(jobId)); + + JsonClient client = new JsonClient(AccessToken, SharedHttpClient); + + var jobInfoUpdate = new IngestJobInfoUpdate + { + State = "UploadComplete" + }; + + var uri = UriFormatter.Job(InstanceUrl, ApiVersion, JobType.Ingest, jobId); + + return await client.HttpPatchAsync(jobInfoUpdate, uri); + } + + public async Task GetIngestJobInfoResultAsync(string jobId) + { + if (string.IsNullOrEmpty(jobId)) throw new ArgumentNullException(nameof(jobId)); + + var uri = UriFormatter.Job(InstanceUrl, ApiVersion, JobType.Ingest, jobId); + + JsonClient client = new JsonClient(AccessToken, SharedHttpClient); + + return await client.HttpGetAsync(uri); + } + + public async Task> GetIngestJobResultAsync(string jobId, IngestJobResultType ingestJobResultType, Func> converter) + { + if (string.IsNullOrEmpty(jobId)) throw new ArgumentNullException(nameof(jobId)); + + var path = ""; + + switch (ingestJobResultType) + { + case IngestJobResultType.Successful: + path = "successfulResults"; + break; + case IngestJobResultType.Failed: + path = "failedResults"; + break; + case IngestJobResultType.Unprocessed: + path = "unprocessedrecords"; + break; ; + } + + var uri = UriFormatter.Job(InstanceUrl, ApiVersion, JobType.Ingest, jobId, path); + + JsonClient client = new JsonClient(AccessToken, SharedHttpClient); + + var result = await client.HttpGetAsync(uri); + IngestJobResult response; + + response = new IngestJobResult + { + Items = converter(result) + }; + + return response; + } + + #endregion + #region metadata /// diff --git a/src/NetCoreForce.Client/JsonClient.cs b/src/NetCoreForce.Client/JsonClient.cs index 0e4a7c6..229e576 100644 --- a/src/NetCoreForce.Client/JsonClient.cs +++ b/src/NetCoreForce.Client/JsonClient.cs @@ -1,18 +1,14 @@ +using NetCoreForce.Client.Models; +using Newtonsoft.Json; using System; -using System.IO; -using System.IO.Compression; -using System.Linq; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using NetCoreForce.Client.Serializer; -using NetCoreForce.Client.Models; namespace NetCoreForce.Client { @@ -64,7 +60,7 @@ public JsonClient(string accessToken, HttpClient httpClient = null) } /// - /// + /// /// /// /// @@ -78,7 +74,7 @@ public async Task HttpGetAsync(Uri uri, Dictionary customH } /// - /// + /// /// /// /// @@ -119,7 +115,7 @@ public async Task HttpPostAsync( /// Serializes ALL object properties to include in the request, even those not appropriate for some update/patch calls. /// includes the SObject ID when serializing the request content /// A list of properties that should be set to null, but inclusing the null values in the serialized output - /// Use with caution. By default null values are not serialized, this will serialize all explicitly nulled or missing properties as null + /// Use with caution. By default null values are not serialized, this will serialize all explicitly nulled or missing properties as null /// /// public async Task HttpPatchAsync( @@ -152,7 +148,7 @@ public async Task HttpPatchAsync( } /// - /// + /// /// /// /// @@ -169,7 +165,7 @@ public async Task HttpDeleteAsync(Uri uri, Dictionary cust return await GetResponse(request, customHeaders, deserializeResponse); } - private async Task HttpAsync(Uri uri, HttpMethod httpMethod, HttpContent content = null, Dictionary customHeaders = null, bool deserializeResponse = true) + public async Task HttpAsync(Uri uri, HttpMethod httpMethod, HttpContent content = null, Dictionary customHeaders = null, bool deserializeResponse = true) { HttpRequestMessage request = new HttpRequestMessage(); request.Headers.Authorization = _authHeaderValue; @@ -254,6 +250,16 @@ private async Task GetResponse(HttpRequestMessage request, Dictionary(responseMessage, responseContent); + } + + if (typeof(T) == typeof(string)) + { + return (T)(object)responseContent; + } + return JsonConvert.DeserializeObject(responseContent); } if (responseMessage.StatusCode == HttpStatusCode.MultipleChoices) @@ -287,9 +293,9 @@ private async Task GetResponse(HttpRequestMessage request, Dictionary GetResponse(HttpRequestMessage request, Dictionary(HttpResponseMessage responseMessage, string responseContent) + { + var locator = GetHeaderValues(responseMessage.Headers, "Sforce-Locator").FirstOrDefault(); + if (locator == "null") + { + locator = null; + } + return (T)(object)new QueryJobResult + { + NumberOfRecords = int.TryParse(GetHeaderValues(responseMessage.Headers, "Sforce-NumberOfRecords").FirstOrDefault(), out var tempNumberOfRecords) ? tempNumberOfRecords : 0, + Locator = locator, + Items = responseContent, + }; + } + /// /// Get values for a particular reponse header /// diff --git a/src/NetCoreForce.Client/Models/BulkConstants.cs b/src/NetCoreForce.Client/Models/BulkConstants.cs new file mode 100644 index 0000000..e4a936c --- /dev/null +++ b/src/NetCoreForce.Client/Models/BulkConstants.cs @@ -0,0 +1,69 @@ +namespace NetCoreForce.Client.Models +{ + public static class BulkConstants + { + public sealed class OperationType + { + public static readonly OperationType Insert = new OperationType("insert"); + public static readonly OperationType Update = new OperationType("update"); + public static readonly OperationType Upsert = new OperationType("upsert"); + public static readonly OperationType Delete = new OperationType("delete"); + public static readonly OperationType HardDelete = new OperationType("hardDelete"); + + private readonly string _value; + + private OperationType(string value) + { + _value = value; + } + + public string Value() + { + return _value; + } + } + + public sealed class BatchState + { + public static readonly BatchState Queued = new BatchState("Open"); + public static readonly BatchState UploadComplete = new BatchState("UploadComplete"); + public static readonly BatchState InProgress = new BatchState("InProgress"); + public static readonly BatchState Completed = new BatchState("Completed"); + public static readonly BatchState Failed = new BatchState("Failed"); + public static readonly BatchState NotProcessed = new BatchState("Not Processed"); + + private readonly string _value; + + private BatchState(string value) + { + _value = value; + } + + public string Value() + { + return _value; + } + } + + public sealed class QueryJobState + { + public static readonly QueryJobState UploadComplete = new QueryJobState("UploadComplete"); + public static readonly QueryJobState InProgress = new QueryJobState("InProgress"); + public static readonly QueryJobState Aborted = new QueryJobState("Aborted"); + public static readonly QueryJobState JobComplete = new QueryJobState("JobComplete"); + public static readonly QueryJobState Failed = new QueryJobState("Failed"); + + private readonly string _value; + + private QueryJobState(string value) + { + _value = value; + } + + public string Value() + { + return _value; + } + } + } +} diff --git a/src/NetCoreForce.Client/Models/CompositeSObjectCollection.cs b/src/NetCoreForce.Client/Models/CompositeSObjectCollection.cs new file mode 100644 index 0000000..3e90b47 --- /dev/null +++ b/src/NetCoreForce.Client/Models/CompositeSObjectCollection.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace NetCoreForce.Client.Models +{ + public class CompositeSObjectCollection : SObject + { + [JsonProperty(PropertyName = "allOrNone")] + public bool AllOrNone { get; set; } = true; + + [JsonProperty(PropertyName = "records")] + public List Records { get; set; } + } +} diff --git a/src/NetCoreForce.Client/Models/IngestJobInfo.cs b/src/NetCoreForce.Client/Models/IngestJobInfo.cs new file mode 100644 index 0000000..b20c581 --- /dev/null +++ b/src/NetCoreForce.Client/Models/IngestJobInfo.cs @@ -0,0 +1,28 @@ +using Newtonsoft.Json; + +namespace NetCoreForce.Client.Models +{ + public class IngestJobInfo + { + [JsonProperty(PropertyName = "assignmentRuleId")] + public string AssignmentRuleId { get; set; } + + [JsonProperty(PropertyName = "columnDelimiter")] + public string ColumnDelimiter { get; set; } = "COMMA"; + + [JsonProperty(PropertyName = "contentType")] + public string ContentType { get; set; } = "CSV"; + + [JsonProperty(PropertyName = "externalIdFieldName")] + public string ExternalIdFieldName { get; set; } + + [JsonProperty(PropertyName = "lineEnding")] + public string LineEnding { get; set; } = "CRLF"; + + [JsonProperty(PropertyName = "object")] + public string Object { get; set; } + + [JsonProperty(PropertyName = "operation")] + public string Operation { get; set; } + } +} diff --git a/src/NetCoreForce.Client/Models/IngestJobInfoResult.cs b/src/NetCoreForce.Client/Models/IngestJobInfoResult.cs new file mode 100644 index 0000000..9c495cf --- /dev/null +++ b/src/NetCoreForce.Client/Models/IngestJobInfoResult.cs @@ -0,0 +1,68 @@ +using Newtonsoft.Json; +using System; + +namespace NetCoreForce.Client.Models +{ + public class IngestJobInfoResult + { + [JsonProperty(PropertyName = "id")] + public string Id { get; set; } + + [JsonProperty(PropertyName = "assignmentRuleId")] + public string AssignmentRuleId { get; set; } + + [JsonProperty(PropertyName = "jobType")] + public string JobType { get; set; } + + [JsonProperty(PropertyName = "operation")] + public string Operation { get; set; } + + [JsonProperty(PropertyName = "object")] + public string Object { get; set; } + + [JsonProperty(PropertyName = "createdById")] + public string CreatedById { get; set; } + + [JsonProperty(PropertyName = "createdDate")] + public DateTime CreatedDate { get; set; } + + [JsonProperty(PropertyName = "systemModstamp")] + public DateTime SystemModstamp { get; set; } + + [JsonProperty(PropertyName = "state")] + public string State { get; set; } + + [JsonProperty(PropertyName = "concurrencyMode")] + public string ConcurrencyMode { get; set; } + + [JsonProperty(PropertyName = "contentType")] + public string ContentType { get; set; } + + [JsonProperty(PropertyName = "contentUrl")] + public string ContentUrl { get; set; } + + [JsonProperty(PropertyName = "externalIdFieldName")] + public string ExternalIdFieldName { get; set; } + + [JsonProperty(PropertyName = "apiVersion")] + public double ApiVersion { get; set; } + + [JsonProperty(PropertyName = "lineEnding")] + public string LineEnding { get; set; } + + [JsonProperty(PropertyName = "columnDelimiter")] + public string ColumnDelimiter { get; set; } + + [JsonProperty(PropertyName = "numberRecordsProcessed")] + public long? NumberRecordsProcessed { get; set; } + + [JsonProperty(PropertyName = "retries")] + public int? Retries { get; set; } + + [JsonProperty(PropertyName = "totalProcessingTime")] + public long? TotalProcessingTime { get; set; } + + [JsonProperty(PropertyName = "isPkChunkingSupported")] + public bool IsPkChunkingSupported { get; set; } + } +} diff --git a/src/NetCoreForce.Client/Models/IngestJobInfoUpdate.cs b/src/NetCoreForce.Client/Models/IngestJobInfoUpdate.cs new file mode 100644 index 0000000..51b9e67 --- /dev/null +++ b/src/NetCoreForce.Client/Models/IngestJobInfoUpdate.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace NetCoreForce.Client.Models +{ + public class IngestJobInfoUpdate + { + [JsonProperty(PropertyName = "state")] + public string State { get; set; } + } +} diff --git a/src/NetCoreForce.Client/Models/IngestJobResult.cs b/src/NetCoreForce.Client/Models/IngestJobResult.cs new file mode 100644 index 0000000..fa5f1be --- /dev/null +++ b/src/NetCoreForce.Client/Models/IngestJobResult.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace NetCoreForce.Client.Models +{ + internal class IngestJobResult + { + + internal string Items { get; set; } + + } + public class IngestJobResult + { + public List Items { get; set; } + } +} diff --git a/src/NetCoreForce.Client/Models/QueryJobInfo.cs b/src/NetCoreForce.Client/Models/QueryJobInfo.cs new file mode 100644 index 0000000..1396376 --- /dev/null +++ b/src/NetCoreForce.Client/Models/QueryJobInfo.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace NetCoreForce.Client.Models +{ + public class QueryJobInfo + { + [JsonProperty(PropertyName = "operation")] + public string Operation { get; set; } + + [JsonProperty(PropertyName = "query")] + public string Query { get; set; } + + [JsonProperty(PropertyName = "contentType")] + public string ContentType { get; set; } = "CSV"; + + [JsonProperty(PropertyName = "lineEnding")] + public string LineEnding { get; set; } = "CRLF"; + + [JsonProperty(PropertyName = "columnDelimiter")] + public string ColumnDelimiter { get; set; } = "COMMA"; + } +} diff --git a/src/NetCoreForce.Client/Models/QueryJobInfoResult.cs b/src/NetCoreForce.Client/Models/QueryJobInfoResult.cs new file mode 100644 index 0000000..3cb5541 --- /dev/null +++ b/src/NetCoreForce.Client/Models/QueryJobInfoResult.cs @@ -0,0 +1,56 @@ +using Newtonsoft.Json; +using System; + +namespace NetCoreForce.Client.Models +{ + public class QueryJobInfoResult + { + [JsonProperty(PropertyName = "id")] + public string Id { get; set; } + + [JsonProperty(PropertyName = "operation")] + public string Operation { get; set; } + + [JsonProperty(PropertyName = "object")] + public string Object { get; set; } + + [JsonProperty(PropertyName = "createdById")] + public string CreatedById { get; set; } + + [JsonProperty(PropertyName = "createdDate")] + public DateTime CreatedDate { get; set; } + + [JsonProperty(PropertyName = "systemModstamp")] + public DateTime SystemModstamp { get; set; } + + [JsonProperty(PropertyName = "state")] + public string State { get; set; } + + [JsonProperty(PropertyName = "concurrencyMode")] + public string ConcurrencyMode { get; set; } + + [JsonProperty(PropertyName = "contentType")] + public string ContentType { get; set; } + + [JsonProperty(PropertyName = "apiVersion")] + public double ApiVersion { get; set; } + + [JsonProperty(PropertyName = "lineEnding")] + public string LineEnding { get; set; } + + [JsonProperty(PropertyName = "columnDelimiter")] + public string ColumnDelimiter { get; set; } + + [JsonProperty(PropertyName = "numberRecordsProcessed")] + public long? NumberRecordsProcessed { get; set; } + + [JsonProperty(PropertyName = "retries")] + public int? Retries { get; set; } + + [JsonProperty(PropertyName = "totalProcessingTime")] + public long? TotalProcessingTime { get; set; } + + [JsonProperty(PropertyName = "isPkChunkingSupported")] + public bool IsPkChunkingSupported { get; set; } + } +} diff --git a/src/NetCoreForce.Client/Models/QueryJobResult.cs b/src/NetCoreForce.Client/Models/QueryJobResult.cs new file mode 100644 index 0000000..802b886 --- /dev/null +++ b/src/NetCoreForce.Client/Models/QueryJobResult.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +namespace NetCoreForce.Client.Models +{ + internal class QueryJobResult + { + internal int NumberOfRecords { get; set; } + + internal string Locator { get; set; } + + internal string Items { get; set; } + } + + public class QueryJobResult + { + internal QueryJobResult(QueryJobResult baseResult) + { + NumberOfRecords = baseResult.NumberOfRecords; + Locator = baseResult.Locator; + } + + public int NumberOfRecords { get; set; } + + public string Locator { get; set; } + + public List Items { get; set; } + } +} \ No newline at end of file diff --git a/src/NetCoreForce.Client/Models/UpdateMultipleResponse.cs b/src/NetCoreForce.Client/Models/UpdateMultipleResponse.cs new file mode 100644 index 0000000..2a612da --- /dev/null +++ b/src/NetCoreForce.Client/Models/UpdateMultipleResponse.cs @@ -0,0 +1,38 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace NetCoreForce.Client.Models +{ + public class UpdateMultipleResponse + { + /// + /// Id + /// + [JsonProperty(PropertyName = "id")] + public string Id { get; set; } + + /// + /// Success flag + /// + [JsonProperty(PropertyName = "success")] + public bool Success { get; set; } + + /// + /// Errors + /// + [JsonProperty(PropertyName = "errors")] + public List Errors { get; set; } + } + + public class UpdateMultipleError + { + [JsonProperty(PropertyName = "statusCode")] + public string StatusCode { get; set; } + + [JsonProperty(PropertyName = "message")] + public string Message { get; set; } + + [JsonProperty(PropertyName = "fields")] + public List Fields { get; set; } + } +} diff --git a/src/NetCoreForce.Client/UriFormatter.cs b/src/NetCoreForce.Client/UriFormatter.cs index 7e4585e..c40d5fd 100644 --- a/src/NetCoreForce.Client/UriFormatter.cs +++ b/src/NetCoreForce.Client/UriFormatter.cs @@ -1,3 +1,4 @@ +using NetCoreForce.Client.Enumerations; using NetCoreForce.Client.Models; using System; using System.Collections.Generic; @@ -17,7 +18,7 @@ public static class UriFormatter /// public static Uri BaseUri(string instanceUrl) { - if (string.IsNullOrEmpty(instanceUrl)) throw new ArgumentNullException(nameof(instanceUrl)); + if (string.IsNullOrEmpty(instanceUrl)) throw new ArgumentNullException("instanceUrl"); // e.g. https://na99.salesforce.com/services/data @@ -32,8 +33,8 @@ public static Uri BaseUri(string instanceUrl) /// public static Uri ApexUri(string instanceUrl, string apexResourceUrl) { - if (string.IsNullOrEmpty(instanceUrl)) throw new ArgumentNullException(nameof(instanceUrl)); - if (string.IsNullOrEmpty(apexResourceUrl)) throw new ArgumentNullException(nameof(apexResourceUrl)); + if (string.IsNullOrEmpty(instanceUrl)) throw new ArgumentNullException("instanceUrl"); + if (string.IsNullOrEmpty(apexResourceUrl)) throw new ArgumentNullException("apexResourceUrl"); // format: / @@ -48,7 +49,7 @@ public static Uri ApexUri(string instanceUrl, string apexResourceUrl) /// public static Uri Versions(string instanceUrl) { - if (string.IsNullOrEmpty(instanceUrl)) throw new ArgumentNullException(nameof(instanceUrl)); + if (string.IsNullOrEmpty(instanceUrl)) throw new ArgumentNullException("instanceUrl"); // format: / @@ -65,8 +66,8 @@ public static Uri Versions(string instanceUrl) /// public static Uri Limits(string instanceUrl, string apiVersion) { - if (string.IsNullOrEmpty(instanceUrl)) throw new ArgumentNullException(nameof(instanceUrl)); - if (string.IsNullOrEmpty(apiVersion)) throw new ArgumentNullException(nameof(apiVersion)); + if (string.IsNullOrEmpty(instanceUrl)) throw new ArgumentNullException("instanceUrl"); + if (string.IsNullOrEmpty(apiVersion)) throw new ArgumentNullException("apiVersion"); //format: /vXX.X/limits/ @@ -83,7 +84,7 @@ public static Uri Limits(string instanceUrl, string apiVersion) /// public static Uri LimitsResource(string apiVersion) { - if (string.IsNullOrEmpty(apiVersion)) throw new ArgumentNullException(nameof(apiVersion)); + if (string.IsNullOrEmpty(apiVersion)) throw new ArgumentNullException("apiVersion"); return new Uri($"{apiVersion}/limits", UriKind.Relative); } @@ -95,8 +96,8 @@ public static Uri LimitsResource(string apiVersion) /// public static Uri DescribeGlobal(string instanceUrl, string apiVersion) { - if (string.IsNullOrEmpty(instanceUrl)) throw new ArgumentNullException(nameof(instanceUrl)); - if (string.IsNullOrEmpty(apiVersion)) throw new ArgumentNullException(nameof(apiVersion)); + if (string.IsNullOrEmpty(instanceUrl)) throw new ArgumentNullException("instanceUrl"); + if (string.IsNullOrEmpty(apiVersion)) throw new ArgumentNullException("apiVersion"); //format: /vXX.X/sobjects/ @@ -111,9 +112,9 @@ public static Uri DescribeGlobal(string instanceUrl, string apiVersion) /// public static Uri SObjectBasicInformation(string instanceUrl, string apiVersion, string sObjectName) { - if (string.IsNullOrEmpty(sObjectName)) throw new ArgumentNullException(nameof(sObjectName)); - if (string.IsNullOrEmpty(instanceUrl)) throw new ArgumentNullException(nameof(instanceUrl)); - if (string.IsNullOrEmpty(apiVersion)) throw new ArgumentNullException(nameof(apiVersion)); + if (string.IsNullOrEmpty(sObjectName)) throw new ArgumentNullException("sObjectName"); + if (string.IsNullOrEmpty(instanceUrl)) throw new ArgumentNullException("instanceUrl"); + if (string.IsNullOrEmpty(apiVersion)) throw new ArgumentNullException("apiVersion"); //format: /vXX.X/sobjects/SObjectName/ @@ -128,9 +129,9 @@ public static Uri SObjectBasicInformation(string instanceUrl, string apiVersion, /// public static Uri SObjectDescribe(string instanceUrl, string apiVersion, string sObjectName) { - if (string.IsNullOrEmpty(sObjectName)) throw new ArgumentNullException(nameof(sObjectName)); - if (string.IsNullOrEmpty(instanceUrl)) throw new ArgumentNullException(nameof(instanceUrl)); - if (string.IsNullOrEmpty(apiVersion)) throw new ArgumentNullException(nameof(apiVersion)); + if (string.IsNullOrEmpty(sObjectName)) throw new ArgumentNullException("sObjectName"); + if (string.IsNullOrEmpty(instanceUrl)) throw new ArgumentNullException("instanceUrl"); + if (string.IsNullOrEmpty(apiVersion)) throw new ArgumentNullException("apiVersion"); //format: /vXX.X/sobjects/SObjectName/describe/ @@ -157,10 +158,10 @@ public static Uri SObjectDescribe(string instanceUrl, string apiVersion, string /// public static Uri SObjectRows(string instanceUrl, string apiVersion, string sObjectName, string objectId, List fields = null) { - if (string.IsNullOrEmpty(instanceUrl)) throw new ArgumentNullException(nameof(instanceUrl)); - if (string.IsNullOrEmpty(apiVersion)) throw new ArgumentNullException(nameof(apiVersion)); - if (string.IsNullOrEmpty(sObjectName)) throw new ArgumentNullException(nameof(sObjectName)); - if (string.IsNullOrEmpty(objectId)) throw new ArgumentNullException(nameof(objectId)); + if (string.IsNullOrEmpty(instanceUrl)) throw new ArgumentNullException("instanceUrl"); + if (string.IsNullOrEmpty(apiVersion)) throw new ArgumentNullException("apiVersion"); + if (string.IsNullOrEmpty(sObjectName)) throw new ArgumentNullException("sObjectName"); + if (string.IsNullOrEmpty(objectId)) throw new ArgumentNullException("objectId"); //https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_sobject_retrieve.htm @@ -187,8 +188,8 @@ public static Uri SObjectRows(string instanceUrl, string apiVersion, string sObj /// public static Uri SObjectsComposite(string instanceUrl, string apiVersion) { - if (string.IsNullOrEmpty(instanceUrl)) throw new ArgumentNullException(nameof(instanceUrl)); - if (string.IsNullOrEmpty(apiVersion)) throw new ArgumentNullException(nameof(apiVersion)); + if (string.IsNullOrEmpty(instanceUrl)) throw new ArgumentNullException("instanceUrl"); + if (string.IsNullOrEmpty(apiVersion)) throw new ArgumentNullException("apiVersion"); //format: /vXX.X/composite/sobjects @@ -206,8 +207,8 @@ public static Uri SObjectsComposite(string instanceUrl, string apiVersion) /// public static Uri CompositeRequest(string instanceUrl, string apiVersion) { - if (string.IsNullOrEmpty(instanceUrl)) throw new ArgumentNullException(nameof(instanceUrl)); - if (string.IsNullOrEmpty(apiVersion)) throw new ArgumentNullException(nameof(apiVersion)); + if (string.IsNullOrEmpty(instanceUrl)) throw new ArgumentNullException("instanceUrl"); + if (string.IsNullOrEmpty(apiVersion)) throw new ArgumentNullException("apiVersion"); Uri uri = new Uri(BaseUri(instanceUrl), $"{apiVersion}/composite"); @@ -220,8 +221,8 @@ public static Uri CompositeRequest(string instanceUrl, string apiVersion) /// public static string CompositeSubRequest(string apiVersion, string sObjectName, string objectId) { - if (string.IsNullOrEmpty(sObjectName)) throw new ArgumentNullException(nameof(sObjectName)); - if (string.IsNullOrEmpty(apiVersion)) throw new ArgumentNullException(nameof(apiVersion)); + if (string.IsNullOrEmpty(sObjectName)) throw new ArgumentNullException("sObjectName"); + if (string.IsNullOrEmpty(apiVersion)) throw new ArgumentNullException("apiVersion"); //format: /services/data/vXX.X/sobjects/SObjectName/ @@ -243,9 +244,9 @@ public static string CompositeSubRequest(string apiVersion, string sObjectName, /// public static Uri SObjectTree(string instanceUrl, string apiVersion, string sObjectName) { - if (string.IsNullOrEmpty(instanceUrl)) throw new ArgumentNullException(nameof(instanceUrl)); - if (string.IsNullOrEmpty(apiVersion)) throw new ArgumentNullException(nameof(apiVersion)); - if (string.IsNullOrEmpty(sObjectName)) throw new ArgumentNullException(nameof(sObjectName)); + if (string.IsNullOrEmpty(instanceUrl)) throw new ArgumentNullException("instanceUrl"); + if (string.IsNullOrEmpty(apiVersion)) throw new ArgumentNullException("apiVersion"); + if (string.IsNullOrEmpty(sObjectName)) throw new ArgumentNullException("sObjectName"); //format: /vXX.X/composite/tree/sObjectName @@ -270,12 +271,12 @@ public static Uri SObjectTree(string instanceUrl, string apiVersion, string sObj /// public static Uri SObjectRowsByExternalId(string instanceUrl, string apiVersion, string sObjectName, string fieldName, string fieldValue) { - if (string.IsNullOrEmpty(instanceUrl)) throw new ArgumentNullException(nameof(instanceUrl)); - if (string.IsNullOrEmpty(apiVersion)) throw new ArgumentNullException(nameof(apiVersion)); - if (string.IsNullOrEmpty(sObjectName)) throw new ArgumentNullException(nameof(sObjectName)); - if (string.IsNullOrEmpty(sObjectName)) throw new ArgumentNullException(nameof(sObjectName)); - if (string.IsNullOrEmpty(fieldName)) throw new ArgumentNullException(nameof(fieldName)); - if (string.IsNullOrEmpty(fieldName)) throw new ArgumentNullException(nameof(fieldValue)); + if (string.IsNullOrEmpty(instanceUrl)) throw new ArgumentNullException("instanceUrl"); + if (string.IsNullOrEmpty(apiVersion)) throw new ArgumentNullException("apiVersion"); + if (string.IsNullOrEmpty(sObjectName)) throw new ArgumentNullException("sObjectName"); + if (string.IsNullOrEmpty(sObjectName)) throw new ArgumentNullException("sObjectName"); + if (string.IsNullOrEmpty(fieldName)) throw new ArgumentNullException("fieldName"); + if (string.IsNullOrEmpty(fieldName)) throw new ArgumentNullException("fieldValue"); //format: /vXX.X/sobjects/SObjectName/fieldName/fieldValue @@ -291,7 +292,7 @@ public static Uri SObjectRowsByExternalId(string instanceUrl, string apiVersion, /// public static string CompositeSObjectCollectionsSubRequest(string apiVersion) { - if (string.IsNullOrEmpty(apiVersion)) throw new ArgumentNullException(nameof(apiVersion)); + if (string.IsNullOrEmpty(apiVersion)) throw new ArgumentNullException("apiVersion"); //format: /services/data/vXX.X/composite/sobjects/ @@ -308,11 +309,11 @@ public static string CompositeSObjectCollectionsSubRequest(string apiVersion) /// public static Uri SObjectCollectionsUpsert(string instanceUrl, string apiVersion, string sObjectName, string fieldName) { - if (string.IsNullOrEmpty(instanceUrl)) throw new ArgumentNullException(nameof(instanceUrl)); - if (string.IsNullOrEmpty(apiVersion)) throw new ArgumentNullException(nameof(apiVersion)); - if (string.IsNullOrEmpty(sObjectName)) throw new ArgumentNullException(nameof(sObjectName)); - if (string.IsNullOrEmpty(sObjectName)) throw new ArgumentNullException(nameof(sObjectName)); - if (string.IsNullOrEmpty(fieldName)) throw new ArgumentNullException(nameof(fieldName)); + if (string.IsNullOrEmpty(instanceUrl)) throw new ArgumentNullException("instanceUrl"); + if (string.IsNullOrEmpty(apiVersion)) throw new ArgumentNullException("apiVersion"); + if (string.IsNullOrEmpty(sObjectName)) throw new ArgumentNullException("sObjectName"); + if (string.IsNullOrEmpty(sObjectName)) throw new ArgumentNullException("sObjectName"); + if (string.IsNullOrEmpty(fieldName)) throw new ArgumentNullException("fieldName"); //format: /vXX.X/sobjects/SObjectName/fieldName/fieldValue @@ -331,11 +332,11 @@ public static Uri SObjectCollectionsUpsert(string instanceUrl, string apiVersion /// public static Uri SObjectBlobRetrieve(string instanceUrl, string apiVersion, string sObjectName, string objectId, string blobField = "body") { - if (string.IsNullOrEmpty(instanceUrl)) throw new ArgumentNullException(nameof(instanceUrl)); - if (string.IsNullOrEmpty(apiVersion)) throw new ArgumentNullException(nameof(apiVersion)); - if (string.IsNullOrEmpty(sObjectName)) throw new ArgumentNullException(nameof(sObjectName)); - if (string.IsNullOrEmpty(objectId)) throw new ArgumentNullException(nameof(objectId)); - if (string.IsNullOrEmpty(blobField)) throw new ArgumentNullException(nameof(blobField)); + if (string.IsNullOrEmpty(instanceUrl)) throw new ArgumentNullException("instanceUrl"); + if (string.IsNullOrEmpty(apiVersion)) throw new ArgumentNullException("apiVersion"); + if (string.IsNullOrEmpty(sObjectName)) throw new ArgumentNullException("sObjectName"); + if (string.IsNullOrEmpty(objectId)) throw new ArgumentNullException("objectId"); + if (string.IsNullOrEmpty(blobField)) throw new ArgumentNullException("blobField"); //format: /vXX.X/sobjects/SObjectName/id/ @@ -384,8 +385,8 @@ public static Uri Search(string instanceUrl, string apiVersion, string query) /// public static Uri Batch(string instanceUrl, string apiVersion) { - if (string.IsNullOrEmpty(instanceUrl)) throw new ArgumentNullException(nameof(instanceUrl)); - if (string.IsNullOrEmpty(apiVersion)) throw new ArgumentNullException(nameof(apiVersion)); + if (string.IsNullOrEmpty(instanceUrl)) throw new ArgumentNullException("instanceUrl"); + if (string.IsNullOrEmpty(apiVersion)) throw new ArgumentNullException("apiVersion"); //format: /vXX.X/composite/batch @@ -394,6 +395,36 @@ public static Uri Batch(string instanceUrl, string apiVersion) return uri; } + public static Uri CreateJob(string instanceUrl, string apiVersion, JobType jobType) + { + if (string.IsNullOrEmpty(instanceUrl)) throw new ArgumentNullException(nameof(instanceUrl)); + if (string.IsNullOrEmpty(apiVersion)) throw new ArgumentNullException(nameof(apiVersion)); + + //format: /vXX.X/jobs/ingest + + Uri uri = new Uri(BaseUri(instanceUrl), $"{apiVersion}/jobs/{(jobType == JobType.Query ? "query" : "ingest")}"); + + return uri; + } + + public static Uri Job(string instanceUrl, string apiVersion, JobType jobType, string jobId, string path = "") + { + if (string.IsNullOrEmpty(instanceUrl)) throw new ArgumentNullException(nameof(instanceUrl)); + if (string.IsNullOrEmpty(apiVersion)) throw new ArgumentNullException(nameof(apiVersion)); + if (string.IsNullOrEmpty(jobId)) throw new ArgumentNullException(nameof(jobId)); + + //format: /vXX.X/jobs/ingest/{{0}} + + if (!string.IsNullOrEmpty(path) && !path.StartsWith("/")) + { + path = $"/{path}"; + } + + Uri uri = new Uri(BaseUri(instanceUrl), $"{apiVersion}/jobs/{(jobType == JobType.Query ? "query" : "ingest")}/{jobId}{path}"); + + return uri; + } + /// /// Formats an authentication URL for the User-Agent OAuth Authentication Flow /// @@ -411,9 +442,9 @@ public static Uri UserAgentAuthenticationUrl( string state = "", string scope = "") { - if (string.IsNullOrEmpty(loginUrl)) throw new ArgumentNullException(nameof(loginUrl)); - if (string.IsNullOrEmpty(clientId)) throw new ArgumentNullException(nameof(clientId)); - if (string.IsNullOrEmpty(redirectUrl)) throw new ArgumentNullException(nameof(redirectUrl)); + if (string.IsNullOrEmpty(loginUrl)) throw new ArgumentNullException("loginUrl"); + if (string.IsNullOrEmpty(clientId)) throw new ArgumentNullException("clientId"); + if (string.IsNullOrEmpty(redirectUrl)) throw new ArgumentNullException("redirectUrl"); const ResponseTypes responseType = ResponseTypes.Token; @@ -438,6 +469,8 @@ public static Uri UserAgentAuthenticationUrl( return new Uri(url); } + //TODO: parser for redirect url result + /// /// Formats a manual authentication URL for the Web Server Authentication Flow /// @@ -594,4 +627,4 @@ public static Uri RefreshTokenUrl( return new Uri(url); } } -} \ No newline at end of file +}