diff --git a/README.md b/README.md index 178c2a6f..441a1455 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ bal run The `salesforce` connector provides practical examples illustrating usage in various scenarios. Explore these examples below, covering use cases like creating sObjects, retrieving records, and executing bulk operations. -1. [Salesforce REST API use cases](https://github.com/ballerina-platform/module-ballerinax-sfdc/tree/master/examples/rest_api_usecases) - How to employ REST API of Salesforce to carryout varies tasks. +1. [Salesforce REST API use cases](https://github.com/ballerina-platform/module-ballerinax-sfdc/tree/master/examples/rest_api_usecases) - How to employ REST API of Salesforce to carryout various tasks. 2. [Salesforce Bulk API use cases](https://github.com/ballerina-platform/module-ballerinax-sfdc/tree/master/examples/bulk_api_usecases) - How to employ Bulk API of Salesforce to execute Bulk jobs. diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index a9fb8cb9..c2a9ca76 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -2,7 +2,7 @@ distribution = "2201.8.0" org = "ballerinax" name = "salesforce" -version = "8.0.1" +version = "8.0.2" export = ["salesforce", "salesforce.bulk", "salesforce.soap"] license= ["Apache-2.0"] authors = ["Ballerina"] @@ -17,10 +17,10 @@ observabilityIncluded = true graalvmCompatible = true [[platform.java17.dependency]] -path = "../native/build/libs/salesforce-native-8.0.1.jar" +path = "../native/build/libs/salesforce-native-8.0.2-SNAPSHOT.jar" groupId = "io.ballerinax" artifactId = "salesforce" -version = "8.0.1" +version = "8.0.2-SNAPSHOT" [[platform.java17.dependency]] groupId = "com.opencsv" diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index 0ff4b82c..43a55be3 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -377,7 +377,7 @@ modules = [ [[package]] org = "ballerinax" name = "salesforce" -version = "8.0.1" +version = "8.0.2" dependencies = [ {org = "ballerina", name = "http"}, {org = "ballerina", name = "io"}, diff --git a/ballerina/Module.md b/ballerina/Module.md index 9244c15c..7d5e9c38 100644 --- a/ballerina/Module.md +++ b/ballerina/Module.md @@ -16,16 +16,16 @@ Ballerina Salesforce connector supports [Salesforce v59.0 REST API](https://deve Create Connected Apps - - Here we will be using https://test.salesforce.com as we are using sandbox enviorenment. Users can use https://login.salesforce.com for normal usage. + - Here we will be using https://test.salesforce.com as we are using sandbox environment. Users can use https://login.salesforce.com for normal usage. Create Connected Apps -4. After the creation user can get consumer key and secret through clicking on the `Manage Consume Details` button. +4. After the creation user can get consumer key and secret through clicking on the `Manage Consumer Details` button. Consumer Secrets 5. Next step would be to get the token. - - Log in to salesforce in your prefered browser and enter the following url. + - Log in to salesforce in your preferred browser and enter the following url. ``` https://.salesforce.com/services/oauth2/authorize?response_type=code&client_id=&redirect_uri= ``` @@ -71,7 +71,7 @@ salesforce:ConnectionConfig config = { } }; -salesforce:Client salesforce = new(config); +salesforce:Client salesforce = check new (config); ``` #### Step 3: Invoke connector operation @@ -99,7 +99,7 @@ bal run The `salesforce` connector provides practical examples illustrating usage in various scenarios. Explore these examples below, covering use cases like creating sObjects, retrieving records, and executing bulk operations. -1. [Salesforce REST API use cases](https://github.com/ballerina-platform/module-ballerinax-sfdc/tree/master/examples/rest_api_usecases) - How to employ REST API of Salesforce to carryout varies tasks. +1. [Salesforce REST API use cases](https://github.com/ballerina-platform/module-ballerinax-sfdc/tree/master/examples/rest_api_usecases) - How to employ REST API of Salesforce to carryout various tasks. 2. [Salesforce Bulk API use cases](https://github.com/ballerina-platform/module-ballerinax-sfdc/tree/master/examples/bulk_api_usecases) - How to employ Bulk API of Salesforce to execute Bulk jobs. diff --git a/ballerina/Package.md b/ballerina/Package.md index 755e9fc3..6e3c1808 100644 --- a/ballerina/Package.md +++ b/ballerina/Package.md @@ -16,16 +16,16 @@ Ballerina Salesforce connector supports [Salesforce v59.0 REST API](https://deve Create Connected Apps - - Here we will be using https://test.salesforce.com as we are using sandbox enviorenment. Users can use https://login.salesforce.com for normal usage. + - Here we will be using https://test.salesforce.com as we are using sandbox environment. Users can use https://login.salesforce.com for normal usage. Create Connected Apps -4. After the creation user can get consumer key and secret through clicking on the `Manage Consume Details` button. +4. After the creation user can get consumer key and secret through clicking on the `Manage Consumer Details` button. Consumer Secrets 5. Next step would be to get the token. - - Log in to salesforce in your prefered browser and enter the following url. + - Log in to salesforce in your preferred browser and enter the following url. ```https://.salesforce.com/services/oauth2/authorize?response_type=code&client_id=&redirect_uri=``` - Allow access if an alert pops up and the browser will be redirected to a Url like follows. @@ -65,7 +65,7 @@ salesforce:ConnectionConfig config = { } }; -salesforce:Client salesforce = new(config); +salesforce:Client salesforce = check new (config); ``` #### Step 3: Invoke connector operation @@ -97,7 +97,7 @@ The `salesforce` integration samples illustrate its usage in various integration The `salesforce` connector provides practical examples illustrating usage in various scenarios. Explore these examples below, covering use cases like creating sObjects, retrieving records, and executing bulk operations. -1. [Salesforce REST API use cases](https://github.com/ballerina-platform/module-ballerinax-sfdc/tree/master/examples/rest_api_usecases) - How to employ REST API of Salesforce to carryout varies tasks. +1. [Salesforce REST API use cases](https://github.com/ballerina-platform/module-ballerinax-sfdc/tree/master/examples/rest_api_usecases) - How to employ REST API of Salesforce to carryout various tasks. 2. [Salesforce Bulk API use cases](https://github.com/ballerina-platform/module-ballerinax-sfdc/tree/master/examples/bulk_api_usecases) - How to employ Bulk API of Salesforce to execute Bulk jobs. diff --git a/ballerina/client.bal b/ballerina/client.bal index 9f3b57fc..07cb5776 100644 --- a/ballerina/client.bal +++ b/ballerina/client.bal @@ -27,6 +27,7 @@ import ballerinax/salesforce.utils; public isolated client class Client { private final http:Client salesforceClient; + private map sfLocators = {}; # Initializes the connector. During initialization you can pass either http:BearerTokenConfig if you have a bearer # token or http:OAuth2RefreshTokenGrantConfig if you have Oauth tokens. @@ -90,7 +91,7 @@ public isolated client class Client { //Describe Organization # Lists summary details about each REST API version available. # - # + return - List of `Version` if successful. Else, the occured `error` + # + return - List of `Version` if successful. Else, the occurred `error` isolated remote function getApiVersions() returns Version[]|error { string path = utils:prepareUrl([BASE_PATH]); return check self.salesforceClient->get(path); @@ -400,10 +401,10 @@ public isolated client class Client { return check self.salesforceClient->get(path); } - # Executes up to 25 subrequests in a single request. + # Executes up to 25 sub-requests in a single request. # # + batchRequests - A record containing all the requests - # + haltOnError - If true, the request halts when an error occurs on an individual subrequest + # + haltOnError - If true, the request halts when an error occurs on an individual sub-request # + return - `BatchResult` if successful or else `error` isolated remote function batch(Subrequest[] batchRequests, boolean haltOnError = false) returns BatchResult|error { string path = utils:prepareUrl([API_BASE_PATH, COMPOSITE, BATCH]); @@ -651,18 +652,57 @@ public isolated client class Client { } - # Get query job results + # Get bulk query job results + # # + bulkJobId - Id of the bulk job - # + return - string[][] if successful else `error` - isolated remote function getQueryResult(string bulkJobId) - returns string[][]|error { - string path = utils:prepareUrl([API_BASE_PATH, JOBS, QUERY, bulkJobId, RESULT]); + # + maxRecords - The maximum number of records to retrieve per set of results for the query + # + return - The resulting string[][] if successful else `error` + isolated remote function getQueryResult(string bulkJobId, int? maxRecords = ()) returns string[][]|error { + + string path = ""; + string batchingParams = ""; + + if maxRecords != () { + lock { + if self.sfLocators.hasKey(bulkJobId) { + string locator = self.sfLocators.get(bulkJobId); + if locator is "null" { + return []; + } + batchingParams = string `results?maxRecords=${maxRecords}&locator=${locator}`; + } else { + batchingParams = string `results?maxRecords=${maxRecords}`; + } + } + path = utils:prepareUrl([API_BASE_PATH, JOBS, QUERY, bulkJobId, batchingParams]); + // Max records value default, we might not know when the locator comes + } else { + lock { + if self.sfLocators.hasKey(bulkJobId) { + string locator = self.sfLocators.get(bulkJobId); + if locator is "null" { + return []; + } + batchingParams = string `results?locator=${locator}`; + path = utils:prepareUrl([API_BASE_PATH, JOBS, QUERY, bulkJobId, batchingParams]); + } else { + path = utils:prepareUrl([API_BASE_PATH, JOBS, QUERY, bulkJobId, RESULT]); + } + } + } + http:Response response = check self.salesforceClient->get(path); if response.statusCode == 200 { string textPayload = check response.getTextPayload(); if textPayload == "" { return []; } + lock { + string|http:HeaderNotFoundError locatorValue = response.getHeader("sforce-locator"); + if locatorValue is string { + self.sfLocators[bulkJobId] = locatorValue; + } // header not found error ignored + } string[][] result = check parseCsvString(textPayload); return result; } else { diff --git a/ballerina/main.bal b/ballerina/main.bal deleted file mode 100644 index 68a42f4c..00000000 --- a/ballerina/main.bal +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) 2021 WSO2 Inc. (http://www.wso2.org) All Rights Reserved. -// -// WSO2 Inc. licenses this file to you under the Apache License, -// Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. diff --git a/ballerina/modules/bulk/Module.md b/ballerina/modules/bulk/Module.md index 76153f78..13ff25b5 100644 --- a/ballerina/modules/bulk/Module.md +++ b/ballerina/modules/bulk/Module.md @@ -15,16 +15,16 @@ This module supports [Salesforce Bulk API v1](https://developer.salesforce.com/d Create Connected Apps - - Here we will be using https://test.salesforce.com as we are using sandbox enviorenment. Users can use https://login.salesforce.com for normal usage. + - Here we will be using https://test.salesforce.com as we are using sandbox environment. Users can use https://login.salesforce.com for normal usage. Create Connected Apps -4. After the creation user can get consumer key and secret through clicking on the `Manage Consume Details` button. +4. After the creation user can get consumer key and secret through clicking on the `Manage Consumer Details` button. Consumer Secrets 5. Next step would be to get the token. - - Log in to salesforce in your prefered browser and enter the following url. + - Log in to salesforce in your preferred browser and enter the following url. `https://.salesforce.com/services/oauth2/authorize?response_type=code&client_id=&redirect_uri=` - Allow access if an alert pops up and the browser will be redirected to a Url like follows. `https://login.salesforce.com/?code=` @@ -64,7 +64,7 @@ bulk:ConnectionConfig sfConfig = { } }; -bulk:Client bulkClient = new (sfConfig); +bulk:Client bulkClient = check new (sfConfig); ``` ### Step 3: Invoke connector operation diff --git a/ballerina/modules/soap/Module.md b/ballerina/modules/soap/Module.md index 09126e46..bff3912f 100644 --- a/ballerina/modules/soap/Module.md +++ b/ballerina/modules/soap/Module.md @@ -15,16 +15,16 @@ This module supports [Salesforce v48.0 SOAP API Enterprise WDSL](https://develop Create Connected Apps - - Here we will be using https://test.salesforce.com as we are using sandbox enviorenment. Users can use https://login.salesforce.com for normal usage. + - Here we will be using https://test.salesforce.com as we are using sandbox environment. Users can use https://login.salesforce.com for normal usage. Create Connected Apps -4. After the creation user can get consumer key and secret through clicking on the `Manage Consume Details` button. +4. After the creation user can get consumer key and secret through clicking on the `Manage Consumer Details` button. Consumer Secrets 5. Next step would be to get the token. - - Log in to salesforce in your prefered browser and enter the following url. + - Log in to salesforce in your preferred browser and enter the following url. `https://.salesforce.com/services/oauth2/authorize?response_type=code&client_id=&redirect_uri=` - Allow access if an alert pops up and the browser will be redirected to a Url like follows. `https://login.salesforce.com/?code=` @@ -61,7 +61,7 @@ soap:ConnectionConfig config = { } }; -soap:Client salesforce = new(sfConfig); +soap:Client salesforce = check new (config); ``` ### Step 3: Invoke connector operation diff --git a/ballerina/tests/bulk_csv_query.bal b/ballerina/tests/bulk_csv_query.bal index ae6ac29c..58db85a3 100644 --- a/ballerina/tests/bulk_csv_query.bal +++ b/ballerina/tests/bulk_csv_query.bal @@ -61,9 +61,121 @@ function queryCsv() returns error? { } } } +} + + +@test:Config { + enable: true, + dependsOn: [insertCsvFromFile, insertCsv, insertCsvStringArrayFromFile, insertCsvStreamFromFile] +} +function queryWithLowerMaxRecordsValue() returns error? { + runtime:sleep(delayInSecs); + log:printInfo("baseClient -> queryCsv"); + string queryStr = "SELECT Id, Name FROM Contact WHERE Title='Professor Level 02'"; + + BulkCreatePayload payloadq = { + operation : "query", + query : queryStr + }; + + //create job + BulkJob queryJob = check baseClient->createQueryJob(payloadq); + int totalRecordsReceived = 0; + int totalIterationsOfGetResult = 0; + string[][]|error batchResult = []; + + //get batch result + foreach int currentRetry in 1 ..< maxIterations + 1 { + while true { + batchResult = baseClient->getQueryResult(queryJob.id, 5); + if batchResult is error || batchResult.length() == 0 { + break; + } else { + totalRecordsReceived += batchResult.length(); + totalIterationsOfGetResult += 1; + } + } + + if totalIterationsOfGetResult != 0 { + if totalRecordsReceived == 7 { + test:assertTrue(totalIterationsOfGetResult == 2, msg = "Retrieving batch result failed."); + break; + } else { + if currentRetry != maxIterations { + log:printWarn("getBatchResult Operation Failed! Retrying..."); + runtime:sleep(delayInSecs); + } else { + log:printWarn("getBatchResult Operation Failed! Giving up after 5 tries."); + } + } + } else if batchResult is error { + if currentRetry != maxIterations { + log:printWarn("getBatchResult Operation Failed! Retrying..."); + runtime:sleep(delayInSecs); + } else { + log:printWarn("getBatchResult Operation Failed! Giving up after 5 tries."); + test:assertFail(msg = batchResult.message()); + } + } + } +} + +@test:Config { + enable: true, + dependsOn: [insertCsvFromFile, insertCsv, insertCsvStringArrayFromFile, insertCsvStreamFromFile] } +function queryWithHigherMaxRecordsValue() returns error? { + runtime:sleep(delayInSecs); + log:printInfo("baseClient -> queryCsv"); + string queryStr = "SELECT Id, Name FROM Contact WHERE Title='Professor Level 02'"; + + BulkCreatePayload payloadq = { + operation : "query", + query : queryStr + }; + + //create job + BulkJob queryJob = check baseClient->createQueryJob(payloadq); + int totalRecordsReceived = 0; + int totalIterationsOfGetResult = 0; + string[][]|error batchResult = []; + //get batch result + foreach int currentRetry in 1 ..< maxIterations + 1 { + while true { + batchResult = baseClient->getQueryResult(queryJob.id, 10); + if batchResult is error || batchResult.length() == 0 { + break; + } else { + totalRecordsReceived += batchResult.length(); + totalIterationsOfGetResult += 1; + } + } + + if totalIterationsOfGetResult != 0 { + if totalRecordsReceived == 7 { + test:assertTrue(totalIterationsOfGetResult == 1, msg = "Retrieving batch result failed."); + break; + } else { + if currentRetry != maxIterations { + log:printWarn("getBatchResult Operation Failed! Retrying..."); + runtime:sleep(delayInSecs); + } else { + log:printWarn("getBatchResult Operation Failed! Giving up after 5 tries."); + } + } + } else if batchResult is error { + if currentRetry != maxIterations { + log:printWarn("getBatchResult Operation Failed! Retrying..."); + runtime:sleep(delayInSecs); + } else { + log:printWarn("getBatchResult Operation Failed! Giving up after 5 tries."); + test:assertFail(msg = batchResult.message()); + } + } + } +} @test:Config { enable: true, diff --git a/changelog.md b/changelog.md new file mode 100644 index 00000000..764892fc --- /dev/null +++ b/changelog.md @@ -0,0 +1,14 @@ +# Change Log +This file contains all the notable changes done to the Ballerina Salesforce package through the releases. + +## [Unreleased] + +### Added + +- [[#6577] Add support for pagination/chunking](https://github.com/ballerina-platform/ballerina-library/issues/6577) + +## [8.0.1] - 2024-05-29 + +### Fixed + +- [[#6556] Fixed connector returning invalid CSV data when values contain new lines](https://github.com/ballerina-platform/ballerina-library/issues/6556)