From 390173560b7d633bd69ad4501ac04456d906238e Mon Sep 17 00:00:00 2001 From: aashikam Date: Wed, 17 Jul 2024 10:27:33 +0530 Subject: [PATCH] Add apex API to apex submodule --- ballerina/Ballerina.toml | 2 +- ballerina/modules/apex/Module.md | 108 ++++++ ballerina/modules/apex/client.bal | 100 ++++++ ballerina/modules/apex/constants.bal | 144 ++++++++ ballerina/modules/apex/data_mappings.bal | 111 ++++++ ballerina/modules/apex/errors.bal | 42 +++ ballerina/modules/apex/tests/README.md | 65 ++++ ballerina/modules/apex/tests/test.bal | 114 +++++++ ballerina/modules/apex/types.bal | 417 +++++++++++++++++++++++ ballerina/modules/apex/utils.bal | 101 ++++++ 10 files changed, 1203 insertions(+), 1 deletion(-) create mode 100644 ballerina/modules/apex/Module.md create mode 100644 ballerina/modules/apex/client.bal create mode 100644 ballerina/modules/apex/constants.bal create mode 100644 ballerina/modules/apex/data_mappings.bal create mode 100644 ballerina/modules/apex/errors.bal create mode 100644 ballerina/modules/apex/tests/README.md create mode 100644 ballerina/modules/apex/tests/test.bal create mode 100644 ballerina/modules/apex/types.bal create mode 100644 ballerina/modules/apex/utils.bal diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index 8748488e..19745435 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -3,7 +3,7 @@ distribution = "2201.8.0" org = "ballerinax" name = "salesforce" version = "8.0.2" -export = ["salesforce", "salesforce.bulk", "salesforce.soap","salesforce.bulkv2"] +export = ["salesforce", "salesforce.bulk", "salesforce.soap","salesforce.bulkv2", "salesforce.apex"] license= ["Apache-2.0"] authors = ["Ballerina"] keywords = ["Sales & CRM/Customer Relationship Management", "Cost/Freemium"] diff --git a/ballerina/modules/apex/Module.md b/ballerina/modules/apex/Module.md new file mode 100644 index 00000000..7d5e9c38 --- /dev/null +++ b/ballerina/modules/apex/Module.md @@ -0,0 +1,108 @@ +## Overview + +Salesforce Sales Cloud is one of the leading Customer Relationship Management(CRM) software, provided by Salesforce.Inc. Salesforce enable users to efficiently manage sales and customer relationships through its APIs, robust and secure databases, and analytics services. Sales cloud provides serveral API packages to make operations on sObjects and metadata, execute queries and searches, and listen to change events through API calls using REST, SOAP, and CometD protocols. + +Ballerina Salesforce connector supports [Salesforce v59.0 REST API](https://developer.salesforce.com/docs/atlas.en-us.224.0.api_rest.meta/api_rest/intro_what_is_rest_api.htm), [Salesforce v59.0 SOAP API](https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_quickstart_intro.htm), [Salesforce v59.0 APEX REST API](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_rest_intro.htm), [Salesforce v59.0 BULK API](https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/api_asynch_introduction_bulk_api.htm), and [Salesforce v59.0 BULK V2 API](https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/bulk_api_2_0.htm). + +## Setup guide + +1. Create a Salesforce account with the REST capability. + +2. Go to Setup --> Apps --> App Manager + + Setup Side Panel + +3. Create a New Connected App. + + Create Connected Apps + + - 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 Consumer Details` button. + + Consumer Secrets + +5. Next step would be to get the token. + - 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= + ``` + + - The code can be obtained after decoding the encoded code + +6. Get Access and Refresh tokens + - Following request can be sent to obtain the tokens. + + ``` + curl -X POST https://.salesforce.com/services/oauth2/token?code=&grant_type=authorization_code&client_id=&client_secret=&redirect_uri=https://test.salesforce.com/ + ``` + - Tokens can be obtained from the response. + +## Quickstart + +To use the Salesforce connector in your Ballerina application, modify the .bal file as follows: + +#### Step 1: Import connector + +Import the `ballerinax/salesforce` package into the Ballerina project. + +```ballerina +import ballerinax/salesforce; +``` + +#### Step 2: Create a new connector instance + +Create a `salesforce:ConnectionConfig` with the obtained OAuth2 tokens and initialize the connector with it. +```ballerina +salesforce:ConnectionConfig config = { + baseUrl: baseUrl, + auth: { + clientId: clientId, + clientSecret: clientSecret, + refreshToken: refreshToken, + refreshUrl: refreshUrl + } +}; + +salesforce:Client salesforce = check new (config); +``` + +#### Step 3: Invoke connector operation + +1. Now you can utilize the available operations. Note that they are in the form of remote operations. + +Following is an example on how to create a record using the connector. + + ```ballerina + salesforce:CreationResponse response = check + salesforce->create("Account", { + "Name": "IT World", + "BillingCity": "New York" + }); + + ``` + +2. Use following command to compile and run the Ballerina program. + +``` +bal run +```` + +## Examples + +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 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. + +3. [Salesforce Bulk v2 API use cases](https://github.com/ballerina-platform/module-ballerinax-sfdc/tree/master/examples/bulkv2_api_usecases) - How to employ Bulk v2 API to execute an ingest job. + +4. [Salesforce APEX REST API use cases](https://github.com/ballerina-platform/module-ballerinax-sfdc/tree/master/examples/apex_rest_api_usecases) - How to employ APEX REST API to create a case in Salesforce. diff --git a/ballerina/modules/apex/client.bal b/ballerina/modules/apex/client.bal new file mode 100644 index 00000000..16c872d0 --- /dev/null +++ b/ballerina/modules/apex/client.bal @@ -0,0 +1,100 @@ +// Copyright (c) 2020, 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. +import ballerina/http; +import ballerina/io; +import ballerina/jballerina.java; +import ballerina/lang.runtime; +import ballerina/time; +import ballerinax/'client.config; +import ballerinax/salesforce.utils; + +# Ballerina Salesforce connector provides the capability to access Salesforce REST API. +# This connector lets you to perform operations for SObjects, query using SOQL, search using SOSL, and describe SObjects +# and organizational data. + +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. + # Create a Salesforce account and obtain tokens following + # [this guide](https://help.salesforce.com/articleView?id=remoteaccess_authenticate_overview.htm). + # + # + salesforceConfig - Salesforce Connector configuration + # + return - `sfdc:Error` on failure of initialization or else `()` + public isolated function init(ConnectionConfig config) returns error? { + http:Client|http:ClientError|error httpClientResult; + http:ClientConfiguration httpClientConfig = check config:constructHTTPClientConfig(config); + httpClientResult = trap new (config.baseUrl, httpClientConfig); + + if httpClientResult is http:Client { + self.salesforceClient = httpClientResult; + } else { + return error(INVALID_CLIENT_CONFIG); + } + } + + # Access Salesforce APEX resource. + # + # + urlPath - URI path + # + methodType - HTTP method type + # + payload - Payload + # + returnType - The payload type, which is expected to be returned after data binding + # + return - `string|int|record{}` type if successful or else `error` + isolated remote function apexRestExecute(string urlPath, http:Method methodType, + record {} payload = {}, typedesc returnType = <>) + returns returnType|error = @java:Method { + 'class: "io.ballerinax.salesforce.ReadOperationExecutor", + name: "apexRestExecute" + } external; + + private isolated function processApexExecute(typedesc returnType, string urlPath, http:Method methodType, record {} payload) returns record {}|string|int|error? { + string path = utils:prepareUrl([APEX_BASE_PATH, urlPath]); + http:Response response = new; + match methodType { + "GET" => { + response = check self.salesforceClient->get(path); + } + "POST" => { + response = check self.salesforceClient->post(path, payload); + } + "DELETE" => { + response = check self.salesforceClient->delete(path); + } + "PUT" => { + response = check self.salesforceClient->put(path, payload); + } + "PATCH" => { + response = check self.salesforceClient->patch(path, payload); + } + _ => { + return error("Invalid Method"); + } + } + if response.statusCode == 200 || response.statusCode == 201 { + if response.getContentType() == "" { + return; + } + json responsePayload = check response.getJsonPayload(); + return check responsePayload.cloneWithType(returnType); + } else { + json responsePayload = check response.getJsonPayload(); + return error("Error occurred while executing the apex request. ", + httpCode = response.statusCode, details = responsePayload); + } + } +} diff --git a/ballerina/modules/apex/constants.bal b/ballerina/modules/apex/constants.bal new file mode 100644 index 00000000..a465dd5f --- /dev/null +++ b/ballerina/modules/apex/constants.bal @@ -0,0 +1,144 @@ +// Copyright (c) 2023 WSO2 LLC. (http://www.wso2.org) All Rights Reserved. +// +// WSO2 LLC. 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. + +//Latest API Version +# Constant field `API_VERSION`. Holds the value for the Salesforce API version. +public const string API_VERSION = "v59.0"; + +// For URL encoding +# Constant field `ENCODING_CHARSET`. Holds the value for the encoding charset. +const string ENCODING_CHARSET = "utf-8"; + +//Salesforce endpoints +# Constant field `BASE_PATH`. Holds the value for the Salesforce base path/URL. +const string BASE_PATH = "/services/data"; + +# Constant field `API_BASE_PATH`. Holds the value for the Salesforce API base path/URL. +final string API_BASE_PATH = string `${BASE_PATH}/${API_VERSION}`; + +# Constant field `APEX_BASE_PATH`. Holds the value for the Salesforce Apex base path/URL. +final string APEX_BASE_PATH = string `/services/apexrest`; + +# Constant field `QUICK_ACTIONS`. Holds the value quickActions for quick actions resource prefix. +final string QUICK_ACTIONS = "quickActions"; + +# Constant field `ANALYTICS`. Holds the value analytics for analytics resource prefix. +const string ANALYTICS = "analytics"; + +# Constant field `ACTIONS`. Holds the value actions for actions resource prefix. +const string ACTIONS = "actions"; + +# Constant field `NAMED_LAYOUTS`. Holds the value namedlayouts for layout resource prefix. +const string NAMED_LAYOUTS = "namedLayouts"; + +# Constant field `COMPOSITE`. Holds the value composite for composite resource prefix. +const string COMPOSITE = "composite"; + +# Constant field `BATCH`. Holds the value batch for batch resource prefix. +const string BATCH = "batch"; + +# Constant field `BATCHES`. Holds the value batches for bulk resource prefix. +const string BATCHES = "batches"; + +# Constant field `JOBS`. Holds the value jobs for bulk resource prefix. +const string JOBS = "jobs"; + +# Constant field `INGEST`. Holds the value ingest for bulk resource prefix. +const string INGEST = "ingest"; + +# Constant field `INSTANCES`. Holds the value instances for instances resource prefix. +const string INSTANCES = "instances"; + +# Constant field `REPORTS`. Holds the value reports for reports resource prefix. +const string REPORTS = "reports"; + +# Constant field `SOBJECTS`. Holds the value sobjects for get sobject resource prefix. +const string SOBJECTS = "sobjects"; + +# Constant field `PASSWORD`. Holds the value sobjects for get password resource prefix. +const string PASSWORD = "password"; + +# Constant field `USER`. Holds the value sobjects for get USER resource prefix. +const string USER = "User"; + +# Constant field `DELETED` Holds the value deleted for get deleted resource prefix. +const string DELETED = "deleted"; + +# Constant field `UPDATED` Holds the value updated for get updated resource prefix. +const string UPDATED = "updated"; + +# Constant field `LIMITS`. Holds the value limits for get limits resource prefix. +const string LIMITS = "limits"; + +# Constant field `DESCRIBE`. Holds the value describe for describe resource prefix. +const string DESCRIBE = "describe"; + +# Constant field `search`. Holds the value search for SOSL search resource prefix. +const string SEARCH = "search"; + +# Constant field `PLATFORM_ACTION`. Holds the value PlatformAction for resource prefix. +const string PLATFORM_ACTION = "PlatformAction"; + +// Query param names +const string QUERY = "query"; + +// Result param names +const string RESULT = "results"; + +# Constant field `FIELDS`. Holds the value fields for resource prefix. +const string FIELDS = "fields"; + +# Constant field `q`. Holds the value q for query resource prefix. +const string Q = "q"; + +# Constant field `QUESTION_MARK`. Holds the value of "?". +const string QUESTION_MARK = "?"; + +# Constant field `EQUAL_SIGN`. Holds the value of "=". +const string EQUAL_SIGN = "="; + +# Constant field `EMPTY_STRING`. Holds the value of "". +public const string EMPTY_STRING = ""; + +# Constant field `AMPERSAND`. Holds the value of "&". +const string AMPERSAND = "&"; + +# Constant field `FORWARD_SLASH`. Holds the value of "/". +const string FORWARD_SLASH = "/"; + +# Next records URl +const NEXT_RECORDS_URL = "nextRecordsUrl"; + +const ATTRIBUTES = "attributes"; + +// SObjects +# Constant field `ACCOUNT`. Holds the value Account for account object. +const string ACCOUNT = "Account"; + +# Constant field `LEAD`. Holds the value Lead for lead object. +const string LEAD = "Lead"; + +# Constant field `CONTACT`. Holds the value Contact for contact object. +const string CONTACT = "Contact"; + +# Constant field `OPPORTUNITY`. Holds the value Opportunity for opportunity object. +const string OPPORTUNITY = "Opportunity"; + +# Constant field `PRODUCT`. Holds the value Product2 for product object. +const string PRODUCT = "Product2"; + +# Constant field `NEW_LINE`. Holds the value of "\n". +const string NEW_LINE = "\n"; diff --git a/ballerina/modules/apex/data_mappings.bal b/ballerina/modules/apex/data_mappings.bal new file mode 100644 index 00000000..00aca270 --- /dev/null +++ b/ballerina/modules/apex/data_mappings.bal @@ -0,0 +1,111 @@ +// Copyright (c) 2020, 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. + +import ballerina/log; + +isolated function toVersions(json payload) returns Version[]|Error { + Version[] versions = []; + json[] versionsArr = payload; + + foreach json ele in versionsArr { + Version|error ver = ele.cloneWithType(Version); + + if ver is Version { + versions[versions.length()] = ver; + } else { + string errMsg = "Error occurred while constructing Version record."; + log:printError(errMsg + " ele:" + ele.toJsonString(), 'error = ver); + return error Error(errMsg, ver); + } + } + return versions; +} + +type StringMap map; + +isolated function toMapOfStrings(json payload) returns map|Error { + map|error strMap = payload.cloneWithType(StringMap); + + if strMap is map { + return strMap; + } else { + string errMsg = "Error occurred while constructing map."; + log:printError(errMsg + " payload:" + payload.toJsonString(), 'error = strMap); + return error Error(errMsg, strMap); + } +} + +type JsonMap map; + +isolated function toMapOfLimits(json payload) returns map|Error { + map limits = {}; + map|error payloadMap = payload.cloneWithType(JsonMap); + + if payloadMap is error { + string errMsg = "Error occurred while constructing map using json payload."; + log:printError(errMsg + " payload:" + payload.toJsonString(), 'error = payloadMap); + return error Error(errMsg, payloadMap); + } else { + foreach var [key, value] in payloadMap.entries() { + Limit|error lim = value.cloneWithType(Limit); + if lim is Limit { + limits[key] = lim; + } else { + string errMsg = "Error occurred while constructing Limit record."; + log:printError(errMsg + " value:" + value.toJsonString(), 'error = lim); + return error Error(errMsg, lim); + } + } + } + return limits; +} + + +isolated function toSObjectMetaData(json payload) returns SObjectMetaData|Error { + SObjectMetaData|error res = payload.cloneWithType(SObjectMetaData); + + if res is SObjectMetaData { + return res; + } else { + string errMsg = "Error occurred while constructing SObjectMetaData record."; + log:printError(errMsg + " payload:" + payload.toJsonString(), 'error = res); + return error Error(errMsg, res); + } +} + +isolated function toOrganizationMetadata(json payload) returns OrganizationMetadata|Error { + OrganizationMetadata|error res = payload.cloneWithType(OrganizationMetadata); + + if res is OrganizationMetadata { + return res; + } else { + string errMsg = "Error occurred while constructing OrganizationMetadata record."; + log:printError(errMsg + " payload:" + payload.toJsonString(), 'error = res); + return error Error(errMsg, res); + } +} + +isolated function toSObjectBasicInfo(json payload) returns SObjectBasicInfo|Error { + SObjectBasicInfo|error res = payload.cloneWithType(SObjectBasicInfo); + + if res is SObjectBasicInfo { + return res; + } else { + string errMsg = "Error occurred while constructing SObjectBasicInfo record."; + log:printError(errMsg + " payload:" + payload.toJsonString(), 'error = res); + return error Error(errMsg, res); + } +} diff --git a/ballerina/modules/apex/errors.bal b/ballerina/modules/apex/errors.bal new file mode 100644 index 00000000..f314a523 --- /dev/null +++ b/ballerina/modules/apex/errors.bal @@ -0,0 +1,42 @@ +// Copyright (c) 2023 WSO2 LLC. (http://www.wso2.org) All Rights Reserved. +// +// WSO2 LLC. 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. + +# Salesforce connector error. +public type Error error; + +# Additional details extracted from the Http error. +# +# + errorCode - Error code from Salesforce +# + message - Response body with extra information +# +public type ErrorDetails record { + string? errorCode?; + string? message?; +}; + +// Error constants +const string JSON_ACCESSING_ERROR_MSG = "Error occurred while accessing the JSON payload of the response."; +const string XML_ACCESSING_ERROR_MSG = "Error occurred while accessing the XML payload of the response."; +const string TEXT_ACCESSING_ERROR_MSG = "Error occurred while accessing the Text payload of the response."; +const string HTTP_CLIENT_ERROR = "Failed to establish the communication with the upstream server or a data binding failure. Refer error.cause() for more details"; +public const string HTTP_ERROR_MSG = "Error occurred while getting the HTTP response."; +const STATUS_CODE = "statusCode"; +const HEADERS = "headers"; +const BODY = "body"; + + +public const string ERR_EXTRACTING_ERROR_MSG = "Error occured while extracting errors from payload."; +public const string INVALID_CLIENT_CONFIG = "Invalid values provided for client configuration parameters."; diff --git a/ballerina/modules/apex/tests/README.md b/ballerina/modules/apex/tests/README.md new file mode 100644 index 00000000..ede4685d --- /dev/null +++ b/ballerina/modules/apex/tests/README.md @@ -0,0 +1,65 @@ +# Testing Ballerina Salesforce module + +**Obtaining Tokens** + +1. Visit [Salesforce](https://www.salesforce.com) and create a Salesforce Account. +2. Create a connected app and obtain the following credentials: + * Base URL (Endpoint) + * Access Token + * Client ID + * Client Secret + * Refresh Token + * Refresh Token URL + +Note:- When you are setting up the connected app, select the following scopes under Selected OAuth Scopes: + +* Access and manage your data (api) +* Perform requests on your behalf at any time (refresh_token, offline_access) +* Provide access to your data via the Web (web) + +3. Provide the client ID and client secret to obtain the refresh token and access token. For more information on + obtaining OAuth2 credentials, go to + [Salesforce documentation](https://help.salesforce.com/articleView?id=remoteaccess_authenticate_overview.htm). + +**Create external ID field in Salesforce** + +Since External ID field called `My_External_Id__c` is used in the tests, follow below steps to create this external ID +field in the salesforce. + +1. Log in to your salesforce account and go to the `Setup` by clicking on the settings icon in the right side of the + menu. +2. Then in the left side panel, under Platform tools click on the `Objects and Fields` and the click on + `Object Manager`. +3. In the Object Manager page click on the `Contact` since we are going to create a external field for Contact SObject. +4. In the Contact page click on the `Fields & Relationships` and click `New` in the right hand side. +5. Then select `Text` as the Data type and click `Next`. +6. Add `My_External_Id` for "Field Label" and `255` for "Length" and click `Next`. +7. At the end click `Save` and see whether external field is added successfully by checking `Fields & Relationships` + fields. + +**Select Objects for Change Notifications** + +To receive notifications for record changes, select the custom objects and supported standard objects that you are +interested in. From Setup, enter Change Data Capture in the Quick Find box, and click Change Data Capture. Select the +SObject which you want to listen for changes + + +**Running Tests** + +1. Create a `ballerina.conf` inside project root directory and replace values inside quotes (eg: ) with + appropriate values. + ``` + EP_URL="" + ACCESS_TOKEN="" + CLIENT_ID="" + CLIENT_SECRET="" + REFRESH_TOKEN="" + REFRESH_URL="" + SF_USERNAME="" + SF_PASSWORD="" + ``` +2. Run the following command inside repo root folder. + ```bash + $ ballerina test -a --sourceroot sfdc-connector + ``` + \ No newline at end of file diff --git a/ballerina/modules/apex/tests/test.bal b/ballerina/modules/apex/tests/test.bal new file mode 100644 index 00000000..2a950185 --- /dev/null +++ b/ballerina/modules/apex/tests/test.bal @@ -0,0 +1,114 @@ +// Copyright (c) 2023 WSO2 LLC. (http://www.wso2.org) All Rights Reserved. +// +// WSO2 LLC. 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. + + +import ballerina/log; +import ballerina/os; +import ballerina/test; +import ballerina/time; +import ballerina/lang.runtime; + +// Create Salesforce client configuration by reading from environemnt. +configurable string clientId = os:getEnv("CLIENT_ID"); +configurable string clientSecret = os:getEnv("CLIENT_SECRET"); +configurable string refreshToken = os:getEnv("REFRESH_TOKEN"); +configurable string refreshUrl = os:getEnv("REFRESH_URL"); +configurable string baseUrl = os:getEnv("EP_URL"); +configurable string username = ""; +configurable string password = ""; + +string reportInstanceID = ""; + +// Using direct-token config for client configuration +ConnectionConfig sfConfigRefreshCodeFlow = { + baseUrl: baseUrl, + auth: { + clientId: clientId, + clientSecret: clientSecret, + refreshToken: refreshToken, + refreshUrl: refreshUrl + } +}; + +ConnectionConfig sfConfigPasswordFlow = { + baseUrl: baseUrl, + auth: { + password, + username, + tokenUrl: refreshUrl, + clientId: clientId, + clientSecret: clientSecret, + credentialBearer: "POST_BODY_BEARER" + } +}; + +ConnectionConfig sfConfigCredentialsFlow = { + baseUrl: baseUrl, + auth: { + clientId: clientId, + clientSecret: clientSecret, + tokenUrl: refreshUrl + } +}; + +Client baseClient = check new (sfConfigRefreshCodeFlow); + +@test:Config { + enable: true +} +function testApex() returns error? { + log:printInfo("baseClient -> executeApex()"); + string|error caseId = baseClient->apexRestExecute("Cases", "POST", + {"subject" : "Bigfoot Sighting9!", + "status" : "New", + "origin" : "Phone", + "priority" : "Low"}); + if caseId is error { + test:assertFail(msg = caseId.message()); + } + runtime:sleep(5); + record{}|error case = baseClient->apexRestExecute(string `Cases/${caseId}`, "GET", {}); + if case is error { + test:assertFail(msg = case.message()); + } + runtime:sleep(5); + error? deleteResponse = baseClient->apexRestExecute(string `Cases/${caseId}`, "DELETE", {}); + if deleteResponse is error { + test:assertFail(msg = deleteResponse.message()); + } +} + + + +@test:AfterSuite {} +function testDeleteRecordNew() returns error? { + log:printInfo("baseClient -> delete()"); + error? response = baseClient->delete(ACCOUNT, testRecordIdNew); + if response is error { + test:assertFail(msg = response.message()); + } +} + +/////////////////////////////////////////// Helper Functions /////////////////////////////////////////////////////////// + +isolated function countStream(stream resultStream) returns int|error { + int nLines = 0; + check from record {} _ in resultStream + do { + nLines += 1; + }; + return nLines; +} diff --git a/ballerina/modules/apex/types.bal b/ballerina/modules/apex/types.bal new file mode 100644 index 00000000..4faba4a7 --- /dev/null +++ b/ballerina/modules/apex/types.bal @@ -0,0 +1,417 @@ +// Copyright (c) 2023 WSO2 LLC. (http://www.wso2.org) All Rights Reserved. +// +// WSO2 LLC. 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. + +import ballerina/http; +import ballerinax/'client.config; + + + +# Represents status of the bulk jobs +public enum Status { + SUCCESSFUL_RESULTS = "successfulResults", + FAILED_RESULTS = "failedResults" +}; + +public enum JobStateEnum { + OPEN = "Open", + UPLOAD_COMPLETE = "UploadComplete", + IN_PROGRESS = "InProgress", + JOB_COMPLETE = "JobComplete", + ABORTED = "Aborted", + FAILED = "Failed" +}; + +public enum JobType { + BIG_OBJECT_INGEST = "BigObjectIngest", + CLASSIC = "Classic", + V2_INGEST = "V2Ingest" +}; + +public enum BulkOperation { + QUERY = "query", + INGEST = "ingest" +}; + +# Operation type of the bulk job. +public enum Operation { + INSERT = "insert", + UPDATE = "update", + DELETE = "delete", + UPSERT = "upsert", + HARD_DELETE = "hardDelete", + QUERY = "query" +}; + +public enum LineEndingEnum { + LF = "LF", + CRLF = "CRLF" +}; + +public enum ColumnDelimiterEnum { + BACKQUOTE, + CARET, + COMMA, + PIPE, + SEMICOLON, + TAB +}; + +# Represents the Salesforce client configuration. +public type ConnectionConfig record {| + *config:ConnectionConfig; + # The Salesforce endpoint URL + string baseUrl; + # Configurations related to client authentication + http:BearerTokenConfig|config:OAuth2RefreshTokenGrantConfig| + config:OAuth2PasswordGrantConfig|config:OAuth2ClientCredentialsGrantConfig auth; +|}; + +# Defines the Salesforce version type. +public type Version record { + # Label of the Salesforce version + string label; + # URL of the Salesforce version + string url; + # Salesforce version number + string 'version; +}; + +# Defines the Limit type to list limits information for your org. +public type Limit record {| + # The limit total for the org + int Max; + # The total number of calls or events left for the org + int Remaining; + json...; +|}; + +# Defines the Attribute type. +# Contains the attribute information of the resultant record. +public type Attribute record {| + # Type of the resultant record + string 'type; + # URL of the resultant record + string url?; +|}; + +# Metadata for your organization and available to the logged-in user. +public type OrganizationMetadata record {| + # Encoding + string encoding; + # Maximum batch size + int maxBatchSize; + # Available SObjects + SObjectMetaData[] sobjects; + json...; +|}; + +# Metadata for an SObject, including information about each field, URLs, and child relationships. +public type SObjectMetaData record {| + # SObject name + string name; + # Is createable + boolean createable; + # Is deletable + boolean deletable; + # Is updateable + boolean updateable; + # Is queryable + boolean queryable; + # SObject label + string label; + # SObject URLs + map urls; + json...; +|}; + + +# Basic info of a SObject. +public type SObjectBasicInfo record {| + # Metadata related to the SObject + SObjectMetaData objectDescribe; + json...; +|}; + +# Represent the Attributes at SObjectBasicInfo. +public type Attributes record { + # Type of the resultant record + string 'type; + # URL of the resultant record + string url; +}; + + +# Response of object creation. +public type CreationResponse record { + # Created object ID + string id; + # Array of errors + anydata[] errors; + # Success flag + boolean success; +}; + +# Represents a Report. +public type Report record { + # Unique report ID + string id; + # Report display name + string name; + # URL that returns report data + string url; + # URL that retrieves report metadata + string describeUrl; + # Information for each instance of the report that was run asynchronously. + string instancesUrl; +}; + +# Represents an instance of a Report. +public type ReportInstance record { + # Unique ID for a report instance + string id; + # Status of the report run + string status; + # Date and time when an instance of the report run was requested + string requestDate; + # Date, time when the instance of the report run finished + string? completionDate; + # URL where results of the report run for that instance are stored + string url; + # API name of the user that created the instance + string ownerId; + # Indicates if it is queryable + boolean queryable; + # Indicates if it has detailed data + boolean hasDetailRows; +}; + +# Represents attributes of instance of an asynchronous report run. +public type AsyncReportAttributes record { + # Unique ID for an instance of a report that was run + string id; + # Unique report ID + string reportId; + # Display name of the report + string reportName; + # Status of the report run + string status; + # API name of the user that created the instance + string ownerId; + # Date and time when an instance of the report run was requested + string requestDate; + # Format of the resource + string 'type; + # Date, time when the instance of the report run finished + string? completionDate; + # Error message if the instance run failed + string? errorMessage; + # Indicates if it is queryable + boolean queryable; +}; + +# Represents attributes of instance of synchronous report run. +public type SyncReportAttributes record { + # Unique report ID + string reportId; + # Display name of the report + string reportName; + # Format of the resource + string 'type; + # Resource URL to get report metadata + string describeUrl; + # Resource URL to run a report asynchronously + string instancesUrl; +}; + +# Represents result of an asynchronous report run. +public type ReportInstanceResult record { + # Attributes for the instance of the report run + AsyncReportAttributes|SyncReportAttributes attributes; + # Indicates if all report results are returned + boolean allData; + # Collection of summary level data or both detailed and summary level data + map? factMap; + # Collection of column groupings + map? groupingsAcross; + # Collection of row groupings + map? groupingsDown; + # Information about the fields used to build the report + map? reportMetadata; + # Indicates if it has detailed data + boolean hasDetailRows; + # Information on report groupings, summary fields, and detailed data columns + map? reportExtendedMetadata; +}; + +# Represent the metadata of deleted records. +public type DeletedRecordsResult record { + # Array of deleted records + record {|string deletedDate; string id;|}[] deletedRecords; + # The earliest date covered by the results + string earliestDateAvailable; + # The latest date covered by the results + string latestDateCovered; +}; + +# Represent the metadata of updated records. +public type UpdatedRecordsResults record { + # Array of updated record IDs + string[] ids; + # The latest date covered by the results + string latestDateCovered; +}; + +# Represent the password status. +public type PasswordStatus record{ + # Indicates whether the password is expired + boolean isExpired; +}; + + +# Represent the Error response for password access. +public type ErrorResponse record { + # Error message + string message; + # Error code + string errorCode; +}; + +# Represent a quick action. +public type QuickAction record { + # Action enum or ID + string actionEnumOrId; + # Action label + string label; + # Action name + string name; + # Action type + string 'type; + # Action URLs + record{string defaultValues?; string quickAction?; string describe?; string defaultValuesTemplate?;} urls; +}; + + +# Represent a batch execution result. +public type SubRequestResult record { + # Status code of the batch execution + int statusCode; + # Result of the batch execution + json? result; +}; + +# Represent Subrequest of a batch. +public type Subrequest record {| + # Subrequest of a batch + string binaryPartName?; + # Binary part name alias + string binaryPartNameAlias?; + # Method of the subrequest + string method; + # Rich input of the subrequest + record{} richInput?; + # URL of the subrequest + string url; +|}; + +# Represent results of the batch request. +public type BatchResult record { + # Indicates whether the batch request has errors + boolean hasErrors; + # Results of the batch request + SubRequestResult[] results; +}; + + + +# Represents the bulk job creation request payload. +public type BulkCreatePayload record { + # the sObject type of the bulk job + string 'object?; + # the operation type of the bulk job + Operation operation; + # the column delimiter of the payload + ColumnDelimiterEnum columnDelimiter?; + # the content type of the payload + string contentType?; + # the line ending of the payload + LineEndingEnum lineEnding?; + # the external ID field name for upsert operations + string externalIdFieldName?; + # the SOQL query for query operations + string query?; +}; + +# Represents the bulk job creation response. +public type BulkJob record { + *BulkJobCloseInfo; + # The URL to use for uploading the CSV data for the job. + string contentUrl?; + # The line ending of the payload. + string lineEnding?; + # The column delimiter of the payload. + string columnDelimiter?; +}; + +# Represents bulk job related information. +public type BulkJobInfo record { + *BulkJob; + # The number of times that Salesforce attempted to process the job. + int retries?; + # The total time spent processing the job. + int totalProcessingTime?; + # The total time spent processing the job by API. + int apiActiveProcessingTime?; + # The total time spent to process triggers and other processes related to the job data; + int apexProcessingTime?; + # The number of records already processed by the job. + int numberRecordsProcessed?; +}; + + +# Represents bulk job related information when Closed. +public type BulkJobCloseInfo record { + # The ID of the job. + string id; + # The operation type of the job. + string operation; + # The sObject type of the job. + string 'object; + # The ID of the user who created the job. + string createdById; + # The date and time when the job was created. + string createdDate; + # The date and time when the job was finished. + string systemModstamp; + # The state of the job. + string state; + # The concurrency mode of the job. + string concurrencyMode; + # The content type of the payload. + string contentType; + # The API version. + float apiVersion; +}; + + +# Represents output for get all jobs request +public type AllJobs record { + # Indicates whether there are more records to retrieve. + boolean done; + # Array of job records. + BulkJobInfo[] records; + # URL to retrieve the next set of records. + string nextRecordsUrl; +}; diff --git a/ballerina/modules/apex/utils.bal b/ballerina/modules/apex/utils.bal new file mode 100644 index 00000000..370acf22 --- /dev/null +++ b/ballerina/modules/apex/utils.bal @@ -0,0 +1,101 @@ +// Copyright (c) 2023 WSO2 LLC. (http://www.wso2.org) All Rights Reserved. +// +// WSO2 LLC. 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. +import ballerina/io; +import ballerina/log; +import ballerina/time; +import ballerina/jballerina.java; +import ballerina/lang.'string as strings; + +isolated string csvContent = EMPTY_STRING; + +# Remove decimal places from a civil seconds value +# +# + civilTime - a time:civil record +# + return - a time:civil record with decimal places removed +# +isolated function removeDecimalPlaces(time:Civil civilTime) returns time:Civil { + time:Civil result = civilTime; + time:Seconds seconds = (result.second is ()) ? 0 : result.second; + decimal floor = decimal:floor(seconds); + result.second = floor; + return result; +} + +# Convert ReadableByteChannel to string. +# +# + rbc - ReadableByteChannel +# + return - converted string +isolated function convertToString(io:ReadableByteChannel rbc) returns string|error { + byte[] readContent; + string textContent = EMPTY_STRING; + while (true) { + byte[]|io:Error result = rbc.read(1000); + if result is io:EofError { + break; + } else if result is io:Error { + string errMsg = "Error occurred while reading from Readable Byte Channel."; + log:printError(errMsg, 'error = result); + return error(errMsg, result); + } else { + readContent = result; + string|error readContentStr = strings:fromBytes(readContent); + if readContentStr is string { + textContent = textContent + readContentStr; + } else { + string errMsg = "Error occurred while converting readContent byte array to string."; + log:printError(errMsg, 'error = readContentStr); + return error(errMsg, readContentStr); + } + } + } + return textContent; +} + +# Convert string[][] to string. +# +# + stringCsvInput - Multi dimentional array of strings +# + return - converted string +isolated function convertStringListToString(string[][]|stream stringCsvInput) returns string|error { + lock { + csvContent = EMPTY_STRING; + } + if stringCsvInput is string[][] { + foreach var row in stringCsvInput { + lock { + csvContent += row.reduce(isolated function(string s, string t) returns string { + return s.concat(",", t); + }, EMPTY_STRING).substring(1) + NEW_LINE; + } + } + } else { + check stringCsvInput.forEach(isolated function(string[] row) { + lock { + csvContent += row.reduce(isolated function(string s, string t) returns string { + return s.concat(",", t); + }, EMPTY_STRING).substring(1) + NEW_LINE; + + } + }); + } + lock { + return csvContent; + } +} + +isolated function parseCsvString(string stringContent) returns string[][]|error = @java:Method { + 'class: "io.ballerinax.salesforce.CsvParserUtils", + name: "parseCsvToStringArray" +} external;