diff --git a/NAMESPACE b/NAMESPACE index f9b9bbee..13b82996 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -3,6 +3,7 @@ export(VERB_n) export(build_soap_xml_from_list) export(catch_errors) +export(collapse_list_with_dupe_names) export(get_os) export(make_base_metadata_url) export(make_base_rest_url) @@ -68,6 +69,8 @@ export(sf_describe_metadata) export(sf_describe_object_fields) export(sf_describe_objects) export(sf_end_job_bulk) +export(sf_find_duplicates) +export(sf_find_duplicates_by_id) export(sf_get_all_jobs_bulk) export(sf_get_job_bulk) export(sf_get_job_records_bulk) @@ -157,6 +160,7 @@ importFrom(methods,as) importFrom(purrr,map) importFrom(purrr,map_df) importFrom(purrr,map_dfc) +importFrom(purrr,modify_if) importFrom(readr,col_character) importFrom(readr,col_guess) importFrom(readr,cols) diff --git a/NEWS.md b/NEWS.md index 1ee51fd3..5b0770b1 100644 --- a/NEWS.md +++ b/NEWS.md @@ -4,10 +4,12 @@ * Add **RForcecom** backward compatibile version of `rforcecom.getObjectDescription()` * Add `sf_describe_object_fields()` which is a tidyier version of `rforcecom.getObjectDescription()` - * Allow users to control whether bulk query results are kept as all character or - the types are guessed (#12) - * Add `sf_get_all_jobs_bulk()` so that users can see retrieve details for all bulk jobs (#13) + * Allow users to control whether query results are kept as all character or the + types are guessed (#12) + * Add `sf_get_all_jobs_bulk()` so that users can see retrieve details for all + bulk jobs (#13) * Add new utility functions `sf_set_password()` and `sf_reset_password()` (#11) + * Add two new functions to check for duplicates (`sf_find_duplicates()`, `sf_find_duplicates_by_id()`) (#4) ### Bug Fixes @@ -15,6 +17,9 @@ api_type = "Bulk 1.0" * Fix bug where Bulk 1.0 queries that timeout hit an error while trying to abort since that only supported aborting Bulk 2.0 jobs (#13) + * Fix bug that had only production environment logins possible because of hard + coding (@weckstm, #18) + * Make `sf_describe_object_fields()` more robust against nested list elements (#16) --- diff --git a/R/read-metadata.R b/R/read-metadata.R index 7e2b338a..0eb4e737 100644 --- a/R/read-metadata.R +++ b/R/read-metadata.R @@ -73,7 +73,8 @@ sf_read_metadata <- function(metadata_type, object_names, verbose=FALSE){ #' of the fields on that object by returning a tibble with one row per field. #' #' @importFrom readr type_convert cols -#' @importFrom dplyr as_tibble +#' @importFrom dplyr as_tibble +#' @importFrom purrr modify_if #' @template object_name #' @note The tibble only contains the fields that the user can view, as defined by #' the user's field-level security settings. diff --git a/R/utils-org.R b/R/utils-org.R index 19ee4b71..f27c0212 100644 --- a/R/utils-org.R +++ b/R/utils-org.R @@ -270,6 +270,182 @@ sf_list_objects <- function(){ return(response_parsed) } +#' Find Duplicate Records +#' +#' Performs rule-based searches for duplicate records. +#' +#' @importFrom dplyr select rename_at everything matches as_tibble +#' @importFrom readr cols type_convert +#' @importFrom purrr map_df +#' @importFrom xml2 xml_ns_strip xml_find_all +#' @importFrom httr content +#' @param search_criteria \code{list}; a list of fields and their values that would +#' constitute a match. For example, list(FirstName="Marc", Company="Salesforce") +#' @template object_name +#' @template include_record_details +#' @template verbose +#' @return \code{tbl_df} of records found to be duplicates by the match rules +#' @note You must have actived duplicate rules for the supplied object before running +#' this function. The \code{object_name} argument refers to using that object's duplicate +#' rules on the search criteria to determine which records in other objects are duplicates. +#' @examples +#' \dontrun{ +#' # if insert a lead with this email address, what duplicate records exist elsewhere +#' # according to the Lead object's duplicate rules +#' found_dupes <- sf_find_duplicates(search_criteria = list(Email = "bond_john@@grandhotels.com"), +#' object_name = "Lead") +#' +#' # now look across all other objects using the Contact object rules +#' found_dupes <- sf_find_duplicates(search_criteria = list(Email = "bond_john@@grandhotels.com"), +#' object_name = "Contact") +#' } +#' @export +sf_find_duplicates <- function(search_criteria, + object_name, + include_record_details = FALSE, + verbose = FALSE){ + + base_soap_url <- make_base_soap_url() + if(verbose) { + message(base_soap_url) + } + + # build the body + r <- make_soap_xml_skeleton(soap_headers=list(DuplicateRuleHeader = list(includeRecordDetails = tolower(include_record_details)))) + xml_dat <- build_soap_xml_from_list(input_data = search_criteria, + operation = "findDuplicates", + object_name = object_name, + root = r) + + httr_response <- rPOST(url = base_soap_url, + headers = c("SOAPAction"="create", + "Content-Type"="text/xml"), + body = as(xml_dat, "character")) + catch_errors(httr_response) + response_parsed <- content(httr_response, encoding='UTF-8') + + duplicate_entitytype <- response_parsed %>% + xml_ns_strip() %>% + xml_find_all('.//result') %>% + xml_find_all('.//duplicateResults//duplicateRuleEntityType') %>% + xml_text() %>% + head(1) + + which_rules <- response_parsed %>% + xml_ns_strip() %>% + xml_find_all('.//result') %>% + xml_find_all('.//duplicateResults//duplicateRule') %>% + map(xml_text) %>% + unlist() %>% + paste(collapse = ", ") + + message(sprintf("Using %s rules: %s", duplicate_entitytype, which_rules)) + + this_res <- response_parsed %>% + xml_ns_strip() %>% + xml_find_all('.//result') %>% + xml_find_all('.//duplicateResults//matchResults//matchRecords//record') %>% + map_df(xml_nodeset_to_df) %>% + rename_at(.vars = vars(contains("sf:")), + .funs = list(~gsub("sf:", "", .))) %>% + rename_at(.vars = vars(contains("Id1")), + .funs = list(~gsub("Id1", "Id", .))) %>% + select(-matches("^V[0-9]+$")) %>% + # move columns without dot up since those are related entities + select(-matches("\\."), everything()) %>% + type_convert(col_types = cols()) %>% + as_tibble() + + # drop columns which are completely missing. This happens with this function whenever + # a linked object is null for a record, so a column is created "sf:EntityName" that + # is NA for that record and then NA for the other records since it is a non-null entity for them + this_res <- Filter(function(x) !all(is.na(x)), this_res) + + return(this_res) +} + +#' Find Duplicate Records By Id +#' +#' Performs rule-based searches for duplicate records. +#' +#' @template sf_id +#' @template include_record_details +#' @template verbose +#' @return \code{tbl_df} of records found to be duplicates by the match rules +#' @note You must have actived duplicate rules for the supplied object before running +#' this function. This function uses the duplicate rules for the object that has +#' the same type as the input record IDs. For example, if the record Id represents +#' an Account, this function uses the duplicate rules associated with the +#' Account object. +#' @examples +#' \dontrun{ +#' found_dupes <- sf_find_duplicates_by_id(sf_id = "00Q6A00000aABCnZZZ") +#' } +#' @export +sf_find_duplicates_by_id <- function(sf_id, + include_record_details = FALSE, + verbose = FALSE){ + + stopifnot(length(sf_id) == 1) + + base_soap_url <- make_base_soap_url() + if(verbose) { + message(base_soap_url) + } + + # build the body + r <- make_soap_xml_skeleton(soap_headers=list(DuplicateRuleHeader = list(includeRecordDetails = tolower(include_record_details)))) + xml_dat <- build_soap_xml_from_list(input_data = sf_id, + operation = "findDuplicatesByIds", + root = r) + + httr_response <- rPOST(url = base_soap_url, + headers = c("SOAPAction"="create", + "Content-Type"="text/xml"), + body = as(xml_dat, "character")) + catch_errors(httr_response) + response_parsed <- content(httr_response, encoding='UTF-8') + + duplicate_entitytype <- response_parsed %>% + xml_ns_strip() %>% + xml_find_all('.//result') %>% + xml_find_all('.//duplicateResults//duplicateRuleEntityType') %>% + xml_text() %>% + head(1) + + which_rules <- response_parsed %>% + xml_ns_strip() %>% + xml_find_all('.//result') %>% + xml_find_all('.//duplicateResults//duplicateRule') %>% + map(xml_text) %>% + unlist() %>% + paste(collapse = ", ") + + message(sprintf("Using %s rules: %s", duplicate_entitytype, which_rules)) + + this_res <- response_parsed %>% + xml_ns_strip() %>% + xml_find_all('.//result') %>% + xml_find_all('.//duplicateResults//matchResults//matchRecords//record') %>% + map_df(xml_nodeset_to_df) %>% + rename_at(.vars = vars(contains("sf:")), + .funs = list(~gsub("sf:", "", .))) %>% + rename_at(.vars = vars(contains("Id1")), + .funs = list(~gsub("Id1", "Id", .))) %>% + select(-matches("^V[0-9]+$")) %>% + # move columns without dot up since those are related entities + select(-matches("\\."), everything()) %>% + type_convert(col_types = cols()) %>% + as_tibble() + + # drop columns which are completely missing. This happens with this function whenever + # a linked object is null for a record, so a column is created "sf:EntityName" that + # is NA for that record and then NA for the other records since it is a non-null entity for them + this_res <- Filter(function(x) !all(is.na(x)), this_res) + + return(this_res) +} + #' #' Delete from Recycle Bin #' #' #' #' Delete records from the recycle bin immediately. diff --git a/R/utils-xml.R b/R/utils-xml.R index fc165bca..bd5c64e6 100644 --- a/R/utils-xml.R +++ b/R/utils-xml.R @@ -132,8 +132,12 @@ make_soap_xml_skeleton <- function(soap_headers=list(), metadata_ns=FALSE){ if(length(soap_headers)>0){ for(i in 1:length(soap_headers)){ opt_node <- newXMLNode(paste0(ns_prefix, ":", names(soap_headers)[i]), - as.character(soap_headers[[i]]), parent=header_node) + for(j in 1:length(soap_headers[[i]])){ + this_node <- newXMLNode(paste0(ns_prefix, ":", names(soap_headers[[i]])[j]), + as.character(soap_headers[[i]][[j]]), + parent=opt_node) + } } } return(root) @@ -164,7 +168,8 @@ build_soap_xml_from_list <- function(input_data, "delete", "search", "query", "queryMore", "describeSObjects", - "setPassword", "resetPassword"), + "setPassword", "resetPassword", + "findDuplicates", "findDuplicatesByIds"), object_name=NULL, fields=NULL, external_id_fieldname=NULL, @@ -216,7 +221,7 @@ build_soap_xml_from_list <- function(input_data, input_data[1,1], parent=operation_node) - } else if(which_operation %in% c("delete","retrieve")){ + } else if(which_operation %in% c("delete","retrieve","findDuplicatesByIds")){ for(i in 1:nrow(input_data)){ this_node <- newXMLNode("urn:ids", diff --git a/R/utils.R b/R/utils.R index 84ba27f1..76f9c967 100644 --- a/R/utils.R +++ b/R/utils.R @@ -60,7 +60,6 @@ sf_input_data_validation <- function(input_data, operation=''){ # TODO: Automatic date validation # https://developer.salesforce.com/docs/atlas.en-us.api_bulk_v2.meta/api_bulk_v2/datafiles_date_format.htm - # put everything into a data.frame format if it's not already if(!is.data.frame(input_data)){ if(is.null(names(input_data))){ if(!is.list(input_data)){ @@ -77,11 +76,11 @@ sf_input_data_validation <- function(input_data, operation=''){ names(input_data) <- "sObjectType" } - if(operation %in% c("delete", "retrieve") & ncol(input_data) == 1){ + if(operation %in% c("delete", "retrieve", "findDuplicatesByIds") & ncol(input_data) == 1){ names(input_data) <- "Id" } - if(operation %in% c("delete", "update")){ + if(operation %in% c("delete", "update", "findDuplicatesByIds")){ if(any(grepl("^ID$|^IDS$", names(input_data), ignore.case=TRUE))){ idx <- grep("^ID$|^IDS$", names(input_data), ignore.case=TRUE) names(input_data)[idx] <- "Id" @@ -90,4 +89,88 @@ sf_input_data_validation <- function(input_data, operation=''){ } return(input_data) -} \ No newline at end of file +} + +api_headers <- function(api_type=NULL, + AllorNoneHeader=list(allOrNone=FALSE), + AllowFieldTruncationHeader=list(allowFieldTruncation=FALSE), + AssignmentRuleHeader=list(useDefaultRule=TRUE), + CallOptions=list(client=NA, defaultNamespace=NA), + DisableFeedTrackingHeader=list(disableFeedTracking=FALSE), + DuplicateRuleHeader=list(allowSave=FALSE, + includeRecordDetails=FALSE, + runAsCurrentUser=TRUE), + EmailHeader=list(triggerAutoResponseEmail=FALSE, + triggerOtherEmail=FALSE, + triggerUserEmail=TRUE), + LimitInfoHeader=list(current="20", + limit="250", + type="API REQUESTS"), + LocaleOptions=list(language=NA), + LoginScopeHeader=list(organizationId=NA, + portalId=NA), + MruHeader=list(updateMru=FALSE), + OwnerChangeOptions=list(options=list(list(execute=FALSE, + type="EnforceNewOwnerHasReadAccess"), + list(execute=TRUE, + type="KeepSalesTeam"), + list(execute=FALSE, + type="KeepSalesTeamGrantCurrentOwnerReadWriteAccess"), + list(execute=TRUE, + type="TransferOpenActivities"), + list(execute=FALSE, + type="TransferNotesAndAttachments"), + list(execute=TRUE, + type="TransferOtherOpenOpportunities"), + list(execute=TRUE, + type="TransferOwnedOpenOpportunities"), + list(execute=TRUE, + type="TransferContracts"), + list(execute=TRUE, + type="TransferOrders"), + list(execute=TRUE, + type="TransferContacts"))), + PackageVersionHeader=list(packageVersions=NA), + QueryOptions=list(batchSize=500), + SessionHeader=list(sessionId=NA), + UserTerritoryDeleteHeader=list(transferToUserId=NA), + ContentTypeHeader=list(`Content-Type`="application/xml"), + BatchRetryHeader=list(`Sforce-Disable-Batch-Retry`=FALSE), + LineEndingHeader=list(`Sforce-Line-Ending`=NA), + PKChunkingHeader=list(`Sforce-Enable-PKChunking`=FALSE)){ + + # check if its in the supplied and known list + # tailor the search to the API + + api_type <- match.arg(api_type) + + if(!is.null()){ + if(api_type == "SOAP"){ + + } else if(api_type == "REST"){ + + } else if(api_type == "Bulk 1.0"){ + + } else { + # do nothing + } + } + + sf_user_info()$userLocale + + list() +} + + + +# TESTING +# # if x is used, then it must be supplied or given a default +# # Error in zz() : argument "x" is missing, with no default +# zz <- function(x,y){ +# if(missing(x)){ +# x <- 2 +# } +# xx <- x +# return(5) +# } + diff --git a/README.Rmd b/README.Rmd index 2cfaa75b..5a721c08 100644 --- a/README.Rmd +++ b/README.Rmd @@ -21,7 +21,9 @@ knitr::opts_chunk$set( **salesforcer** is an R package that connects to Salesforce Platform APIs using tidy principles. The package implements most actions from the SOAP, REST, Bulk 1.0, -Bulk 2.0, and Metadata APIs. Package features include: +Bulk 2.0, and Metadata APIs. + +Package features include: * OAuth 2.0 (Single Sign On) and Basic (Username-Password) Authentication methods (`sf_auth()`) * CRUD (Create, Retrieve, Update, Delete) methods for records using the SOAP, REST, and Bulk APIs diff --git a/README.md b/README.md index 0f11cd9c..0f324bce 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,52 @@ -salesforcer -================================================================================ - -[![Build Status](https://travis-ci.org/StevenMMortimer/salesforcer.svg?branch=master)](https://travis-ci.org/StevenMMortimer/salesforcer) [![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/github/StevenMMortimer/salesforcer?branch=master&svg=true)](https://ci.appveyor.com/project/StevenMMortimer/salesforcer) [![CRAN\_Status\_Badge](http://www.r-pkg.org/badges/version/salesforcer)](http://cran.r-project.org/package=salesforcer) [![Coverage Status](https://codecov.io/gh/StevenMMortimer/salesforcer/branch/master/graph/badge.svg)](https://codecov.io/gh/StevenMMortimer/salesforcer?branch=master) - -**salesforcer** is an R package that connects to Salesforce Platform APIs using tidy principles. The package implements most actions from the SOAP, REST, Bulk 1.0, Bulk 2.0, and Metadata APIs. Package features include: - -- OAuth 2.0 (Single Sign On) and Basic (Username-Password) Authentication methods (`sf_auth()`) -- CRUD (Create, Retrieve, Update, Delete) methods for records using the SOAP, REST, and Bulk APIs -- Query records via the SOAP, REST, and Bulk 1.0 APIs using `sf_query()` -- Retrieve and modify metadata (Custom Objects, Fields, etc.) using the Metadata API with: - - `sf_describe_objects()`, `sf_create_metadata()`, `sf_update_metadata()` -- Utilize backwards compatible functions for the **RForcecom** package, such as: - - `rforcecom.login()`, `rforcecom.query()`, `rforcecom.create()`, `rforcecom.update()` -- Basic utility calls (`sf_user_info()`, `sf_server_timestamp()`, `sf_list_objects()`) - -Table of Contents ------------------ - -- [Installation](#installation) -- [Usage](#usage) - - [Authenticate](#authenticate) - - [Create](#create) - - [Query](#query) - - [Update](#update) - - [Bulk Operations](#bulk-operations) - - [Using the Metadata API](#using-the-metadata-api) -- [Future](#future) -- [Credits](#credits) -- [More Information](#more-information) - -Installation ------------- +# salesforcer + +[![Build +Status](https://travis-ci.org/StevenMMortimer/salesforcer.svg?branch=master)](https://travis-ci.org/StevenMMortimer/salesforcer) +[![AppVeyor Build +Status](https://ci.appveyor.com/api/projects/status/github/StevenMMortimer/salesforcer?branch=master&svg=true)](https://ci.appveyor.com/project/StevenMMortimer/salesforcer) +[![CRAN\_Status\_Badge](http://www.r-pkg.org/badges/version/salesforcer)](http://cran.r-project.org/package=salesforcer) +[![Coverage +Status](https://codecov.io/gh/StevenMMortimer/salesforcer/branch/master/graph/badge.svg)](https://codecov.io/gh/StevenMMortimer/salesforcer?branch=master) + +**salesforcer** is an R package that connects to Salesforce Platform +APIs using tidy principles. The package implements most actions from the +SOAP, REST, Bulk 1.0, Bulk 2.0, and Metadata APIs. + +Package features include: + + - OAuth 2.0 (Single Sign On) and Basic (Username-Password) + Authentication methods (`sf_auth()`) + - CRUD (Create, Retrieve, Update, Delete) methods for records using + the SOAP, REST, and Bulk APIs + - Query records via the SOAP, REST, and Bulk 1.0 APIs using + `sf_query()` + - Retrieve and modify metadata (Custom Objects, Fields, etc.) using + the Metadata API with: + - `sf_describe_objects()`, `sf_create_metadata()`, + `sf_update_metadata()` + - Utilize backwards compatible functions for the **RForcecom** + package, such as: + - `rforcecom.login()`, `rforcecom.getObjectDescription()`, + `rforcecom.query()`, `rforcecom.create()` + - Basic utility calls (`sf_user_info()`, `sf_server_timestamp()`, + `sf_list_objects()`) + +## Table of Contents + + - [Installation](#installation) + - [Usage](#usage) + - [Authenticate](#authenticate) + - [Create](#create) + - [Query](#query) + - [Update](#update) + - [Bulk Operations](#bulk-operations) + - [Using the Metadata API](#using-the-metadata-api) + - [Future](#future) + - [Credits](#credits) + - [More Information](#more-information) + +## Installation ``` r # install from CRAN @@ -42,19 +57,23 @@ install.packages("salesforcer") devtools::install_github("StevenMMortimer/salesforcer") ``` -If you encounter a clear bug, please file a minimal reproducible example on [GitHub](https://github.com/StevenMMortimer/salesforcer/issues). +If you encounter a clear bug, please file a minimal reproducible example +on [GitHub](https://github.com/StevenMMortimer/salesforcer/issues). -Usage ------ +## Usage ### Authenticate -First, load the **salesforcer** package and login. There are two ways to authenticate: +First, load the **salesforcer** package and login. There are two ways to +authenticate: 1. OAuth 2.0 2. Basic Username-Password -It is recommended to use OAuth 2.0 so that passwords do not have to be shared or embedded within scripts. User credentials will be stored in locally cached file entitled ".httr-oauth-salesforcer" in the current working directory. +It is recommended to use OAuth 2.0 so that passwords do not have to be +shared or embedded within scripts. User credentials will be stored in +locally cached file entitled “.httr-oauth-salesforcer” in the current +working directory. ``` r suppressWarnings(suppressMessages(library(dplyr))) @@ -70,7 +89,9 @@ sf_auth(username = "test@gmail.com", security_token = "{SECURITY_TOKEN_HERE}") ``` -After logging in with `sf_auth()`, you can check your connectivity by looking at the information returned about the current user. It should be information about you! +After logging in with `sf_auth()`, you can check your connectivity by +looking at the information returned about the current user. It should be +information about you\! ``` r # pull down information of person logged in @@ -78,14 +99,16 @@ After logging in with `sf_auth()`, you can check your connectivity by looking at # and confirm a connection to the APIs user_info <- sf_user_info() sprintf("User Id: %s", user_info$id) -#> [1] "User Id: 0056A000000MPRjQAO" +#> character(0) sprintf("User Active?: %s", user_info$isActive) -#> [1] "User Active?: TRUE" +#> character(0) ``` ### Create -Salesforce has objects and those objects contain records. One default object is the "Contact" object. This example shows how to create two records in the Contact object. +Salesforce has objects and those objects contain records. One default +object is the “Contact” object. This example shows how to create two +records in the Contact object. ``` r n <- 2 @@ -95,14 +118,21 @@ created_records <- sf_create(new_contacts, object_name="Contact") created_records #> # A tibble: 2 x 2 #> id success -#> -#> 1 0036A00000Sp4fWQAR true -#> 2 0036A00000Sp4fXQAR true +#> +#> 1 0036A00000tZzYAQA0 TRUE +#> 2 0036A00000tZzYBQA0 TRUE ``` ### Query -Salesforce has proprietary form of SQL called SOQL (Salesforce Object Query Language). SOQL is a powerful tool that allows you to return the attributes of records on almost any object in Salesforce including Accounts, Contacts, Tasks, Opportunities, even Attachments! Below is an example where we grab the data we just created including Account object information for which the Contact record is associated with. The Account column is all `NA` since we have yet to provide information to link these Contacts with Accounts. +Salesforce has proprietary form of SQL called SOQL (Salesforce Object +Query Language). SOQL is a powerful tool that allows you to return the +attributes of records on almost any object in Salesforce including +Accounts, Contacts, Tasks, Opportunities, even Attachments\! Below is an +example where we grab the data we just created including Account object +information for which the Contact record is associated with. The Account +column is all `NA` since we have yet to provide information to link +these Contacts with Accounts. ``` r my_soql <- sprintf("SELECT Id, @@ -117,14 +147,21 @@ queried_records <- sf_query(my_soql) queried_records #> # A tibble: 2 x 4 #> Id Account FirstName LastName -#> * -#> 1 0036A00000Sp4fWQAR NA Test Contact-Create-1 -#> 2 0036A00000Sp4fXQAR NA Test Contact-Create-2 +#> +#> 1 0036A00000tZzYAQA0 NA Test Contact-Create-1 +#> 2 0036A00000tZzYBQA0 NA Test Contact-Create-2 ``` ### Update -After creating records you can update them using `sf_update()`. Updating a record requires you to pass the Salesforce `Id` of the record. Salesforce creates a unique 18-character identifier on each record and uses that to know which record to attach the update information you provide. Simply include a field or column in your update dataset called "Id" and the information will be matched. Here is an example where we update each of the records we created earlier with a new first name called "TestTest". +After creating records you can update them using `sf_update()`. Updating +a record requires you to pass the Salesforce `Id` of the record. +Salesforce creates a unique 18-character identifier on each record and +uses that to know which record to attach the update information you +provide. Simply include a field or column in your update dataset called +“Id” and the information will be matched. Here is an example where we +update each of the records we created earlier with a new first name +called “TestTest”. ``` r # Update some of those records @@ -136,16 +173,34 @@ updated_records <- sf_update(queried_records, object_name="Contact") updated_records #> # A tibble: 2 x 2 #> id success -#> -#> 1 0036A00000Sp4fWQAR true -#> 2 0036A00000Sp4fXQAR true +#> +#> 1 0036A00000tZzYAQA0 TRUE +#> 2 0036A00000tZzYBQA0 TRUE ``` ### Bulk Operations -For really large operations (inserts, updates, upserts, deletes, and queries) Salesforce provides the [Bulk 1.0](https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/asynch_api_intro.htm) and [Bulk 2.0](https://developer.salesforce.com/docs/atlas.en-us.api_bulk_v2.meta/api_bulk_v2/introduction_bulk_api_2.htm) APIs. In order to use the Bulk APIs in **salesforcer** you can just add `api_type = "Bulk 1.0"` or `api_type = "Bulk 2.0"` to your functions and the operation will be executed using the Bulk APIs. It's that simple. - -The benefits of using the Bulk API for larger datasets is that the operation will reduce the number of individual API calls (organization usually have a limit on total calls) and batching the requests in bulk is usually quicker than running thousands of individuals calls when your data is large. **Note:** the Bulk 2.0 API does **NOT** guarantee the order of the data submitted is preserved in the output. This means that you must join on other data columns to match up the Ids that are returned in the output with the data you submitted. For this reason, Bulk 2.0 may not be a good solution for creating, updating, or upserting records where you need to keep track of the created Ids. The Bulk 2.0 API would be fine for deleting records where you only need to know which Ids were successfully deleted. +For really large operations (inserts, updates, upserts, deletes, and +queries) Salesforce provides the +[Bulk 1.0](https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/asynch_api_intro.htm) +and +[Bulk 2.0](https://developer.salesforce.com/docs/atlas.en-us.api_bulk_v2.meta/api_bulk_v2/introduction_bulk_api_2.htm) +APIs. In order to use the Bulk APIs in **salesforcer** you can just add +`api_type = "Bulk 1.0"` or `api_type = "Bulk 2.0"` to your functions and +the operation will be executed using the Bulk APIs. It’s that simple. + +The benefits of using the Bulk API for larger datasets is that the +operation will reduce the number of individual API calls (organization +usually have a limit on total calls) and batching the requests in bulk +is usually quicker than running thousands of individuals calls when your +data is large. **Note:** the Bulk 2.0 API does **NOT** guarantee the +order of the data submitted is preserved in the output. This means that +you must join on other data columns to match up the Ids that are +returned in the output with the data you submitted. For this reason, +Bulk 2.0 may not be a good solution for creating, updating, or upserting +records where you need to keep track of the created Ids. The Bulk 2.0 +API would be fine for deleting records where you only need to know which +Ids were successfully deleted. ``` r # create contacts using the Bulk API @@ -170,7 +225,18 @@ deleted_records <- sf_delete(queried_records$Id, object_name="Contact", api_type ### Using the Metadata API -Salesforce is a very flexible platform. Salesforce provides the [Metadata API](https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_intro.htm) for users to create, read, update and delete the objects, page layouts, and more. This makes it very easy to programmatically setup and teardown the Salesforce environment. One common use case for the Metadata API is retrieving information about an object (fields, permissions, etc.). You can use the `sf_read_metadata()` function to return a list of objects and their metadata. In the example below we retrieve the metadata for the Account and Contact objects. Note that the `metadata_type` argument is "CustomObject". Standard Objects are an implementation of CustomObjects, so they are returned using that metadata type. +Salesforce is a very flexible platform. Salesforce provides the +[Metadata +API](https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_intro.htm) +for users to create, read, update and delete the objects, page layouts, +and more. This makes it very easy to programmatically setup and teardown +the Salesforce environment. One common use case for the Metadata API is +retrieving information about an object (fields, permissions, etc.). You +can use the `sf_read_metadata()` function to return a list of objects +and their metadata. In the example below we retrieve the metadata for +the Account and Contact objects. Note that the `metadata_type` argument +is “CustomObject”. Standard Objects are an implementation of +CustomObjects, so they are returned using that metadata type. ``` r read_obj_result <- sf_read_metadata(metadata_type='CustomObject', @@ -209,34 +275,65 @@ read_obj_result[[1]][first_two_fields_idx] #> [1] "Picklist" ``` -The data is returned as a list because object definitions are highly nested representations. You may notice that we are missing some really specific details, such as, the picklist values of a field with type "Picklist". You can get that information using the function `sf_describe_object()` function which is actually part of the REST and SOAP APIs. It is recommended that you try out the various metadata functions `sf_read_metadata()`, `sf_list_metadata()`, `sf_describe_metadata()`, and `sf_describe_objects()` in order to see which information best suits your use case. +The data is returned as a list because object definitions are highly +nested representations. You may notice that we are missing some really +specific details, such as, the picklist values of a field with type +“Picklist”. You can get that information using the +`sf_describe_object_fields()` or `sf_describe_objects()` functions which +are based on calls from REST and SOAP APIs. Here is an example using +`sf_describe_object_fields()` where we get a `tbl_df` with one row for +each field on the Account object: + +``` r +acct_fields <- sf_describe_object_fields('Account') +acct_fields %>% select(name, label, length, soapType, type) +#> # A tibble: 67 x 5 +#> name label length soapType type +#> +#> 1 Id Account ID 18 tns:ID id +#> 2 IsDeleted Deleted 0 xsd:boolean boolean +#> 3 MasterRecordId Master Record ID 18 tns:ID reference +#> 4 Name Account Name 255 xsd:string string +#> 5 Type Account Type 40 xsd:string picklist +#> 6 ParentId Parent Account ID 18 tns:ID reference +#> 7 BillingStreet Billing Street 255 xsd:string textarea +#> 8 BillingCity Billing City 40 xsd:string string +#> 9 BillingState Billing State/Province 80 xsd:string string +#> 10 BillingPostalCode Billing Zip/Postal Code 20 xsd:string string +#> # … with 57 more rows +``` + +If you prefer to be more precise about collecting and formatting the +field data you can work directly with the nested lists that the APIs +return. In this example we look at the picklist values of fields on the +Account +object. ``` r describe_obj_result <- sf_describe_objects(object_names=c('Account', 'Contact')) +# confirm that the Account object is queryable describe_obj_result[[1]][c('label', 'queryable')] #> $label #> [1] "Account" #> #> $queryable #> [1] "true" -# show the first two returned fields of the Account object -the_type_field <- describe_obj_result[[1]][[58]] -the_type_field$name -#> [1] "Type" +# show the different picklist values for the Account Type field +the_type_field <- describe_obj_result[[1]][[59]] +the_type_field$label +#> NULL map_df(the_type_field[which(names(the_type_field) == "picklistValues")], as_tibble) -#> # A tibble: 7 x 4 -#> active defaultValue label value -#> -#> 1 true false Prospect Prospect -#> 2 true false Customer - Direct Customer - Direct -#> 3 true false Customer - Channel Customer - Channel -#> 4 true false Channel Partner / Reseller Channel Partner / Resell… -#> 5 true false Installation Partner Installation Partner -#> 6 true false Technology Partner Technology Partner -#> 7 true false Other Other +#> # A tibble: 0 x 0 ``` -Where the Metadata API really shines is when it comes to CRUD operations on metadata. In this example we will create an object, add fields to it, then delete that object. +It is recommended that you try out the various metadata functions +`sf_read_metadata()`, `sf_list_metadata()`, `sf_describe_metadata()`, +and `sf_describe_objects()` in order to see which information best suits +your use case. + +Where the Metadata API really shines is when it comes to CRUD operations +on metadata. In this example we will create an object, add fields to it, +then delete that object. ``` r # create an object @@ -269,28 +366,45 @@ deleted_custom_object_result <- sf_delete_metadata(metadata_type = 'CustomObject object_names = c('Custom_Account1__c')) ``` -Future ------- +## Future Future APIs to support: -- [Reports and Dashboards REST API](https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_intro.htm) -- [Analytics REST API](https://developer.salesforce.com/docs/atlas.en-us.bi_dev_guide_rest.meta/bi_dev_guide_rest/bi_rest_overview.htm) - -Credits -------- - -This application uses other open source software components. The authentication components are mostly verbatim copies of the routines established in the **googlesheets** package (). Methods are inspired by the **RForcecom** package (). We acknowledge and are grateful to these developers for their contributions to open source. - -More Information ----------------- - -Salesforce provides client libraries and examples in many programming langauges (Java, Python, Ruby, and PhP) but unfortunately R is not a supported language. However, most all operations supported by the Salesforce APIs are available via this package. This package makes requests best formatted to match what the APIs require as input. This articulation is not perfect and continued progress will be made to add and improve functionality. For details on formatting, attributes, and methods please refer to [Salesforce's documentation](https://developer.salesforce.com/page/Salesforce_APIs) as they are explained better there. - -More information is also available on the `pkgdown` site at . + - [Reports and Dashboards REST + API](https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_intro.htm) + - [Analytics REST + API](https://developer.salesforce.com/docs/atlas.en-us.bi_dev_guide_rest.meta/bi_dev_guide_rest/bi_rest_overview.htm) + +## Credits + +This application uses other open source software components. The +authentication components are mostly verbatim copies of the routines +established in the **googlesheets** package +(). Methods are inspired by the +**RForcecom** package (). We +acknowledge and are grateful to these developers for their contributions +to open source. + +## More Information + +Salesforce provides client libraries and examples in many programming +langauges (Java, Python, Ruby, and PhP) but unfortunately R is not a +supported language. However, most all operations supported by the +Salesforce APIs are available via this package. This package makes +requests best formatted to match what the APIs require as input. This +articulation is not perfect and continued progress will be made to add +and improve functionality. For details on formatting, attributes, and +methods please refer to [Salesforce’s +documentation](https://developer.salesforce.com/page/Salesforce_APIs) as +they are explained better there. + +More information is also available on the `pkgdown` site at +. [Top](#salesforcer) ------------------------------------------------------------------------- +----- -Please note that this project is released with a [Contributor Code of Conduct](CONDUCT.md). By participating in this project you agree to abide by its terms. +Please note that this project is released with a [Contributor Code of +Conduct](CONDUCT.md). By participating in this project you agree to +abide by its terms. diff --git a/docs/articles/getting-started.html b/docs/articles/getting-started.html index 548e708d..53acfae8 100644 --- a/docs/articles/getting-started.html +++ b/docs/articles/getting-started.html @@ -118,129 +118,129 @@

2018-03-12

First, load the salesforcer package and login. There are two ways to authenticate: 1) OAuth 2.0 and 2) Basic Username-Password. It is recommended to use OAuth 2.0 so that passwords do not have to be shared/embedded within scripts. User credentials will be stored in locally cached file entitled “.httr-oauth-salesforcer” in the current working directory.

-
suppressWarnings(suppressMessages(library(dplyr)))
-library(salesforcer)
-sf_auth()
+

Just a note, that it’s not necessary to setup your own Connected App in Salesforce to use OAuth 2.0 authentication. The only difference is that the authentication will be run through the client created and associated with the salesforcer package. By using the package client, you will NOT be giving access to Salesforce to anyone, the package is just the medium for you to connect to your own data. If you wanted more control you would specify those options like so:

-
options(salesforcer.consumer_key = "012345678901-99thisisatest99connected33app22key")
-options(salesforcer.consumer_secret = "Th1s1sMyConsumerS3cr3t")
-
-sf_auth()
+

After logging in with sf_auth(), you can check your connectivity by looking at the information returned about the current user. It should be information about you!

-
# pull down information of person logged in
-# it's a simple easy call to get started 
-# and confirm a connection to the APIs
-user_info <- sf_user_info()
-sprintf("User Id: %s", user_info$id)
-#> character(0)
-sprintf("User Active?: %s", user_info$isActive)
-#> character(0)
+

Create

Salesforce has objects and those objects contain records. One default object is the “Contact” object. This example shows how to create two records in the Contact object.

-
n <- 2
-new_contacts <- tibble(FirstName = rep("Test", n),
-                       LastName = paste0("Contact-Create-", 1:n))
-created_records <- sf_create(new_contacts, "Contact")
-created_records
-#> # A tibble: 2 x 2
-#>   id                 success
-#>   <chr>              <chr>  
-#> 1 0036A00000kHti1QAC true   
-#> 2 0036A00000kHti2QAC true
+

Retrieve

Retrieve pulls down a specific set of records and fields. It’s very similar to running a query, but doesn’t use SOQL. Here is an example where we retrieve the data we just created.

-
retrieved_records <- sf_retrieve(ids=created_records$id, 
-                                 fields=c("FirstName", "LastName"), 
-                                 object_name="Contact")
-retrieved_records
-#> # A tibble: 2 x 3
-#>   Id                 FirstName LastName        
-#>   <chr>              <chr>     <chr>           
-#> 1 0036A00000kHti1QAC Test      Contact-Create-1
-#> 2 0036A00000kHti2QAC Test      Contact-Create-2
+

Query

Salesforce has proprietary form of SQL called SOQL (Salesforce Object Query Language). SOQL is a powerful tool that allows you to return the attributes of records on almost any object in Salesforce including Accounts, Contacts, Tasks, Opportunities, even Attachments! Below is an example where we grab the data we just created including Account object information for which the Contact record is associated with. The Account column is all NA since we have yet to provide information to link these Contacts with Accounts.

-
my_soql <- sprintf("SELECT Id, 
-                           Account.Name, 
-                           FirstName, 
-                           LastName 
-                    FROM Contact 
-                    WHERE Id in ('%s')", 
-                   paste0(created_records$id , collapse="','"))
-
-queried_records <- sf_query(my_soql)
-queried_records
-#> # A tibble: 2 x 4
-#>   Id                 Account FirstName LastName        
-#> * <chr>              <lgl>   <chr>     <chr>           
-#> 1 0036A00000kHti1QAC NA      Test      Contact-Create-1
-#> 2 0036A00000kHti2QAC NA      Test      Contact-Create-2
+

Update

After creating records you can update them using sf_update(). Updating a record requires you to pass the Salesforce Id of the record. Salesforce creates a unique 18-character identifier on each record and uses that to know which record to attach the update information you provide. Simply include a field or column in your update dataset called “Id” and the information will be matched. Here is an example where we update each of the records we created earlier with a new first name called “TestTest”.

-
# Update some of those records
-queried_records <- queried_records %>%
-  mutate(FirstName = "TestTest") %>% 
-  select(-Account)
-
-updated_records <- sf_update(queried_records, object_name="Contact")
-updated_records
-#> # A tibble: 2 x 2
-#>   id                 success
-#>   <chr>              <chr>  
-#> 1 0036A00000kHti1QAC true   
-#> 2 0036A00000kHti2QAC true
+

Delete

You can also delete records in Salesforce. The method implements a “soft” delete meaning that the deleted records go to the Recycle Bin which can be emptied or queried against later in the event that the record needed.

-
deleted_records <- sf_delete(updated_records$id)
-deleted_records
-#> # A tibble: 2 x 3
-#>   id                 success errors    
-#>   <chr>              <lgl>   <list>    
-#> 1 0036A00000kHti1QAC TRUE    <list [0]>
-#> 2 0036A00000kHti2QAC TRUE    <list [0]>
+

Upsert

Finally, Salesforce has a unique method called “upsert” that allows you to create and/or update records at the same time. More specifically, if the record is not found based an an “External Id” field, then Salesforce will create the record instead of updating one. Below is an example where we create 2 records, then upsert 3, where 2 are matched and updated and one is created. NOTE: You will need to create a custom field on the target object and ensure it is labeled as an “External Id” field. Read more at http://blog.jeffdouglas.com/2010/05/07/using-exernal-id-fields-in-salesforce/.

-
n <- 2
-new_contacts <- tibble(FirstName = rep("Test", n),
-                       LastName = paste0("Contact-Create-", 1:n), 
-                       My_External_Id__c=letters[1:n])
-created_records <- sf_create(new_contacts, "Contact")
-
-upserted_contacts <- tibble(FirstName = rep("Test", n),
-                            LastName = paste0("Contact-Upsert-", 1:n), 
-                            My_External_Id__c=letters[1:n])
-new_record <- tibble(FirstName = "Test",
-                     LastName = paste0("Contact-Upsert-", n+1), 
-                     My_External_Id__c=letters[n+1])
-upserted_contacts <- bind_rows(upserted_contacts, new_record)
-
-upserted_records <- sf_upsert(input_data=upserted_contacts, 
-                              object_name="Contact", 
-                              external_id_fieldname="My_External_Id__c")
-upserted_records
-#> # A tibble: 3 x 3
-#>   created id                 success
-#>   <chr>   <chr>              <chr>  
-#> 1 false   0036A00000kHti6QAC true   
-#> 2 false   0036A00000kHti7QAC true   
-#> 3 true    0036A00000kHtiBQAS true
+

diff --git a/docs/articles/transitioning-from-RForcecom.html b/docs/articles/transitioning-from-RForcecom.html index e2ca9235..a8d51799 100644 --- a/docs/articles/transitioning-from-RForcecom.html +++ b/docs/articles/transitioning-from-RForcecom.html @@ -122,183 +122,149 @@

2018-03-12

Authentication

salesforcer supports OAuth 2.0 authentication which is preferred, but for backward compatibility provides the username-password authentication routine implemented by RForcecom. Here is an example running the function from each of the packages side-by-side and producing the same result.

-
# the RForcecom way
-session1 <- RForcecom::rforcecom.login(username, paste0(password, security_token), 
-                                       apiVersion=getOption("salesforcer.api_version"))
-session1['sessionID'] <- "{MASKED}"
-session1
-#>                      sessionID                    instanceURL 
-#>                     "{MASKED}" "https://na50.salesforce.com/" 
-#>                     apiVersion 
-#>                         "42.0"
-
-# replicated in salesforcer package
-session2 <- salesforcer::rforcecom.login(username, paste0(password, security_token), 
-                                         apiVersion=getOption("salesforcer.api_version"))
-session2['sessionID'] <- "{MASKED}"
-session2
-#>                      sessionID                    instanceURL 
-#>                     "{MASKED}" "https://na50.salesforce.com/" 
-#>                     apiVersion 
-#>                         "42.0"
+

Note that we must set the API version here because calls to session will not create a new sessionId and then we are stuck with version 35.0 (the default from RForcecom::rforcecom.login). Some functions in salesforcer implement API calls that are only available after version 35.0.

CRUD Operations

“CRUD” operations (Create, Retrieve, Update, Delete) in the RForcecom package only operate on one record at a time. One benefit to using the salesforcer package is that these operations will accept a named vector (one record) or an entire data.frame or tbl_df of records to churn through. However, rest assured that the replicated functions behave exactly the same way if you are hesitant to making the switch.

-
object <- "Contact"
-fields <- c(FirstName="Test", LastName="Contact-Create-Compatibility")
-
-# the RForcecom way
-result1 <- RForcecom::rforcecom.create(session, objectName=object, fields)
-result1
-#>                   id success
-#> 1 0036A00000kHtiQQAS    true
-
-# replicated in salesforcer package
-result2 <- salesforcer::rforcecom.create(session, objectName=object, fields)
-result2
-#>                   id success
-#> 1 0036A00000kHtiVQAS    true
+

Here is an example showing the reduction in code of using salesforcer if you would like to create multiple records.

-
n <- 2
-new_contacts <- tibble(FirstName = rep("Test", n),
-                       LastName = paste0("Contact-Create-", 1:n))
-
-# the RForcecom way
-rforcecom_results <- NULL
-for(i in 1:nrow(new_contacts)){
-  temp <- RForcecom::rforcecom.create(session, 
-                                      objectName = "Contact", 
-                                      fields = unlist(slice(new_contacts,i)))
-  rforcecom_results <- bind_rows(rforcecom_results, temp)
-}
-rforcecom_results
-#>                   id success
-#> 1 0036A00000kHtiaQAC    true
-#> 2 0036A00000kHtifQAC    true
-
-# the better way in salesforcer to do multiple records
-salesforcer_results <- sf_create(new_contacts, object_name="Contact")
-salesforcer_results
-#> # A tibble: 2 x 2
-#>   id                 success
-#>   <chr>              <chr>  
-#> 1 0036A00000kHtikQAC true   
-#> 2 0036A00000kHtilQAC true
+

Query

salesforcer also has better printing and type-casting when returning query result thanks to features of the readr package.

-
this_soql <- "SELECT Id, Email FROM Contact LIMIT 5"
-
-# the RForcecom way
-result1 <- RForcecom::rforcecom.query(session, soqlQuery = this_soql)
-result1
-#>                   Id
-#> 1 0036A00000kHn9kQAC
-#> 2 0036A00000kHn9lQAC
-#> 3 0036A00000kHnAYQA0
-#> 4 0036A00000kHnAxQAK
-#> 5 0036A00000kHn4pQAC
-
-# replicated in salesforcer package
-result2 <- salesforcer::rforcecom.query(session, soqlQuery = this_soql)
-result2
-#> # A tibble: 5 x 2
-#>   Id                 Email
-#> * <chr>              <lgl>
-#> 1 0036A00000kHn9kQAC NA   
-#> 2 0036A00000kHn9lQAC NA   
-#> 3 0036A00000kHnAYQA0 NA   
-#> 4 0036A00000kHnAxQAK NA   
-#> 5 0036A00000kHn4pQAC NA
-
-# the better way in salesforcer to query
-salesforcer_results <- sf_query(this_soql)
-salesforcer_results
-#> # A tibble: 5 x 2
-#>   Id                 Email
-#> * <chr>              <lgl>
-#> 1 0036A00000kHn9kQAC NA   
-#> 2 0036A00000kHn9lQAC NA   
-#> 3 0036A00000kHnAYQA0 NA   
-#> 4 0036A00000kHnAxQAK NA   
-#> 5 0036A00000kHn4pQAC NA
+

Describe

The RForcecom package has the function rforcecom.getObjectDescription() which returns a data.frame with one row per field on an object. The same function in salesforcer is named sf_describe_object_fields() and also has better printing and datatype casting by using tibbles.

-
# the RForcecom way
-result1 <- RForcecom::rforcecom.getObjectDescription(session, objectName='Account')
-
-# backwards compatible in the salesforcer package
-result2 <- salesforcer::rforcecom.getObjectDescription(session, objectName='Account')
-
-# the better way in salesforcer to get object fields
-result3 <- salesforcer::sf_describe_object_fields('Account')
-result3
-#> # A tibble: 67 x 166
-#>    aggregatable autoNumber byteLength calculated caseSensitive createable
-#>    <chr>        <chr>           <int> <chr>      <chr>         <chr>     
-#>  1 true         false              18 false      false         false     
-#>  2 false        false               0 false      false         false     
-#>  3 true         false              18 false      false         false     
-#>  4 true         false             765 false      false         true      
-#>  5 true         false             120 false      false         true      
-#>  6 true         false              18 false      false         true      
-#>  7 true         false             765 false      false         true      
-#>  8 true         false             120 false      false         true      
-#>  9 true         false             240 false      false         true      
-#> 10 true         false              60 false      false         true      
-#> # ... with 57 more rows, and 160 more variables: custom <chr>,
-#> #   defaultedOnCreate <chr>, deprecatedAndHidden <chr>, digits <int>,
-#> #   filterable <chr>, groupable <chr>, idLookup <chr>, label <chr>,
-#> #   length <int>, name <chr>, nameField <chr>, namePointing <chr>,
-#> #   nillable <chr>, permissionable <chr>, polymorphicForeignKey <chr>,
-#> #   precision <int>, queryByDistance <chr>, restrictedPicklist <chr>,
-#> #   scale <int>, searchPrefilterable <chr>, soapType <chr>,
-#> #   sortable <chr>, type <chr>, unique <chr>, updateable <chr>,
-#> #   defaultValue.text <chr>, defaultValue..attrs <chr>, referenceTo <chr>,
-#> #   relationshipName <chr>, compoundFieldName <chr>, extraTypeInfo <chr>,
-#> #   picklistValues.active <chr>, picklistValues.defaultValue <chr>,
-#> #   picklistValues.label <chr>, picklistValues.value <chr>,
-#> #   picklistValues.active.1 <chr>, picklistValues.defaultValue.1 <chr>,
-#> #   picklistValues.label.1 <chr>, picklistValues.value.1 <chr>,
-#> #   picklistValues.active.2 <chr>, picklistValues.defaultValue.2 <chr>,
-#> #   picklistValues.label.2 <chr>, picklistValues.value.2 <chr>,
-#> #   picklistValues.active.3 <chr>, picklistValues.defaultValue.3 <chr>,
-#> #   picklistValues.label.3 <chr>, picklistValues.value.3 <chr>,
-#> #   picklistValues.active.4 <chr>, picklistValues.defaultValue.4 <chr>,
-#> #   picklistValues.label.4 <chr>, picklistValues.value.4 <chr>,
-#> #   picklistValues.active.5 <chr>, picklistValues.defaultValue.5 <chr>,
-#> #   picklistValues.label.5 <chr>, picklistValues.value.5 <chr>,
-#> #   picklistValues.active.6 <chr>, picklistValues.defaultValue.6 <chr>,
-#> #   picklistValues.label.6 <chr>, picklistValues.value.6 <chr>,
-#> #   picklistValues.active.7 <chr>, picklistValues.defaultValue.7 <chr>,
-#> #   picklistValues.label.7 <chr>, picklistValues.value.7 <chr>,
-#> #   picklistValues.active.8 <chr>, picklistValues.defaultValue.8 <chr>,
-#> #   picklistValues.label.8 <chr>, picklistValues.value.8 <chr>,
-#> #   picklistValues.active.9 <chr>, picklistValues.defaultValue.9 <chr>,
-#> #   picklistValues.label.9 <chr>, picklistValues.value.9 <chr>,
-#> #   picklistValues.active.10 <chr>, picklistValues.defaultValue.10 <chr>,
-#> #   picklistValues.label.10 <chr>, picklistValues.value.10 <chr>,
-#> #   picklistValues.active.11 <chr>, picklistValues.defaultValue.11 <chr>,
-#> #   picklistValues.label.11 <chr>, picklistValues.value.11 <chr>,
-#> #   picklistValues.active.12 <chr>, picklistValues.defaultValue.12 <chr>,
-#> #   picklistValues.label.12 <chr>, picklistValues.value.12 <chr>,
-#> #   picklistValues.active.13 <chr>, picklistValues.defaultValue.13 <chr>,
-#> #   picklistValues.label.13 <chr>, picklistValues.value.13 <chr>,
-#> #   picklistValues.active.14 <chr>, picklistValues.defaultValue.14 <chr>,
-#> #   picklistValues.label.14 <chr>, picklistValues.value.14 <chr>,
-#> #   picklistValues.active.15 <chr>, picklistValues.defaultValue.15 <chr>,
-#> #   picklistValues.label.15 <chr>, picklistValues.value.15 <chr>,
-#> #   picklistValues.active.16 <chr>, picklistValues.defaultValue.16 <chr>,
-#> #   picklistValues.label.16 <chr>, picklistValues.value.16 <chr>,
-#> #   picklistValues.active.17 <chr>, …
+
# the RForcecom way
+result1 <- RForcecom::rforcecom.getObjectDescription(session, objectName='Account')
+
+# backwards compatible in the salesforcer package
+result2 <- salesforcer::rforcecom.getObjectDescription(session, objectName='Account')
+
+# the better way in salesforcer to get object fields
+result3 <- salesforcer::sf_describe_object_fields('Account')
+result3
+#> # A tibble: 67 x 38
+#>    aggregatable autoNumber byteLength calculated caseSensitive createable
+#>    <chr>        <chr>      <chr>      <chr>      <chr>         <chr>     
+#>  1 true         false      18         false      false         false     
+#>  2 false        false      0          false      false         false     
+#>  3 true         false      18         false      false         false     
+#>  4 true         false      765        false      false         true      
+#>  5 true         false      120        false      false         true      
+#>  6 true         false      18         false      false         true      
+#>  7 true         false      765        false      false         true      
+#>  8 true         false      120        false      false         true      
+#>  9 true         false      240        false      false         true      
+#> 10 true         false      60         false      false         true      
+#> # … with 57 more rows, and 32 more variables: custom <chr>,
+#> #   defaultedOnCreate <chr>, deprecatedAndHidden <chr>, digits <chr>,
+#> #   filterable <chr>, groupable <chr>, idLookup <chr>, label <chr>,
+#> #   length <chr>, name <chr>, nameField <chr>, namePointing <chr>,
+#> #   nillable <chr>, permissionable <chr>, polymorphicForeignKey <chr>,
+#> #   precision <chr>, queryByDistance <chr>, restrictedPicklist <chr>,
+#> #   scale <chr>, searchPrefilterable <chr>, soapType <chr>,
+#> #   sortable <chr>, type <chr>, unique <chr>, updateable <chr>,
+#> #   defaultValue <list>, referenceTo <chr>, relationshipName <chr>,
+#> #   compoundFieldName <chr>, extraTypeInfo <chr>, picklistValues <list>,
+#> #   externalId <chr>

In the future more features will be migrated from RForcecom to make the transition as seamless as possible.

diff --git a/docs/articles/working-with-bulk-api.html b/docs/articles/working-with-bulk-api.html index c49975b9..4149ffce 100644 --- a/docs/articles/working-with-bulk-api.html +++ b/docs/articles/working-with-bulk-api.html @@ -121,69 +121,69 @@

2018-03-12

Using the Bulk API

First, load the salesforcer package and login.

-
suppressWarnings(suppressMessages(library(dplyr)))
-library(salesforcer)
-sf_auth()
-

For really large inserts, updates, deletes, upserts, queries you can just add “api_type” = “Bulk” to most functions to get the benefits of using the Bulk API instead of the SOAP or REST APIs. Here is the difference in using the REST API vs. the Bulk API to do an insert:

-
n <- 2
-new_contacts <- tibble(FirstName = rep("Test", n),
-                       LastName = paste0("Contact-Create-", 1:n))
-# REST
-rest_created_records <- sf_create(new_contacts, object_name="Contact", api_type="REST")
-rest_created_records
-#> # A tibble: 2 x 3
-#>   id                 success errors    
-#>   <chr>              <lgl>   <list>    
-#> 1 0036A00000kHtipQAC TRUE    <list [0]>
-#> 2 0036A00000kHtiqQAC TRUE    <list [0]>
-# Bulk
-bulk_created_records <- sf_create(new_contacts, object_name="Contact", api_type="Bulk 1.0")
-bulk_created_records
-#> # A tibble: 2 x 4
-#>   Id                 Success Created Error
-#>   <chr>              <chr>   <chr>   <chr>
-#> 1 0036A00000kHtiuQAC true    true    <NA> 
-#> 2 0036A00000kHtivQAC true    true    <NA>
+ +

For really large inserts, updates, deletes, upserts, queries you can just add “api_type” = “Bulk” to most functions to get the benefits of using the Bulk API instead of the SOAP or REST APIs. Here is the difference in using the REST API vs.  the Bulk API to do an insert:

+

There are some differences in the way each API returns response information; however, the end result is exactly the same for these two calls. Also, note that this package utilizes the Bulk 2.0 API for most bulk calls except for bulk queries since Salesforce has not yet implemented it in 2.0.

Here is a simple workflow of adding, querying, and deleting records using the Bulk 1.0 API.

-
# just add api_type="Bulk 1.0" or api_type="Bulk 2.0" to most calls!
-# create bulk
-object <- "Contact"
-n <- 2
-new_contacts <- tibble(FirstName = rep("Test", n),
-                       LastName = paste0("Contact-Create-", 1:n))
-created_records <- sf_create(new_contacts, object_name=object, api_type="Bulk 1.0")
-created_records
-#> # A tibble: 2 x 4
-#>   Id                 Success Created Error
-#>   <chr>              <chr>   <chr>   <chr>
-#> 1 0036A00000kHtj9QAC true    true    <NA> 
-#> 2 0036A00000kHtjAQAS true    true    <NA>
-
-# query bulk
-my_soql <- sprintf("SELECT Id,
-                           FirstName, 
-                           LastName
-                    FROM Contact 
-                    WHERE Id in ('%s')", 
-                   paste0(created_records$Id , collapse="','"))
-
-queried_records <- sf_query(my_soql, object_name=object, api_type="Bulk 1.0")
-queried_records
-#> # A tibble: 2 x 3
-#>   Id                 FirstName LastName        
-#>   <chr>              <chr>     <chr>           
-#> 1 0036A00000kHtj9QAC Test      Contact-Create-1
-#> 2 0036A00000kHtjAQAS Test      Contact-Create-2
-
-# delete bulk
-deleted_records <- sf_delete(queried_records$Id, object_name=object, api_type="Bulk 1.0")
-deleted_records
-#> # A tibble: 2 x 4
-#>   Id                 Success Created Error
-#>   <chr>              <chr>   <chr>   <chr>
-#> 1 0036A00000kHtj9QAC true    false   <NA> 
-#> 2 0036A00000kHtjAQAS true    false   <NA>
+ diff --git a/docs/index.html b/docs/index.html index 82c6d74b..d4e0a035 100644 --- a/docs/index.html +++ b/docs/index.html @@ -162,12 +162,12 @@

Installation

-
# install from CRAN
-install.packages("salesforcer")
-
-# or get the latest version available on GitHub using the devtools package
-# install.packages("devtools")
-devtools::install_github("StevenMMortimer/salesforcer")
+

If you encounter a clear bug, please file a minimal reproducible example on GitHub.

@@ -182,203 +182,203 @@

  • Basic Username-Password
  • It is recommended to use OAuth 2.0 so that passwords do not have to be shared or embedded within scripts. User credentials will be stored in locally cached file entitled “.httr-oauth-salesforcer” in the current working directory.

    -
    suppressWarnings(suppressMessages(library(dplyr)))
    -suppressWarnings(suppressMessages(library(purrr)))
    -library(salesforcer)
    -
    -# Using OAuth 2.0 authentication
    -sf_auth()
    -
    -# Using Basic Username-Password authentication
    -sf_auth(username = "test@gmail.com", 
    -        password = "{PASSWORD_HERE}",
    -        security_token = "{SECURITY_TOKEN_HERE}")
    +

    After logging in with sf_auth(), you can check your connectivity by looking at the information returned about the current user. It should be information about you!

    -
    # pull down information of person logged in
    -# it's a simple easy call to get started 
    -# and confirm a connection to the APIs
    -user_info <- sf_user_info()
    -sprintf("User Id: %s", user_info$id)
    -#> character(0)
    -sprintf("User Active?: %s", user_info$isActive)
    -#> character(0)
    +

    Create

    Salesforce has objects and those objects contain records. One default object is the “Contact” object. This example shows how to create two records in the Contact object.

    -
    n <- 2
    -new_contacts <- tibble(FirstName = rep("Test", n),
    -                       LastName = paste0("Contact-Create-", 1:n))
    -created_records <- sf_create(new_contacts, object_name="Contact")
    -created_records
    -#> # A tibble: 2 x 2
    -#>   id                 success
    -#>   <chr>              <chr>  
    -#> 1 0036A00000kHth8QAC true   
    -#> 2 0036A00000kHth9QAC true
    +

    Query

    Salesforce has proprietary form of SQL called SOQL (Salesforce Object Query Language). SOQL is a powerful tool that allows you to return the attributes of records on almost any object in Salesforce including Accounts, Contacts, Tasks, Opportunities, even Attachments! Below is an example where we grab the data we just created including Account object information for which the Contact record is associated with. The Account column is all NA since we have yet to provide information to link these Contacts with Accounts.

    -
    my_soql <- sprintf("SELECT Id, 
    -                           Account.Name, 
    -                           FirstName, 
    -                           LastName 
    -                    FROM Contact 
    -                    WHERE Id in ('%s')", 
    -                   paste0(created_records$id , collapse="','"))
    -
    -queried_records <- sf_query(my_soql)
    -queried_records
    -#> # A tibble: 2 x 4
    -#>   Id                 Account FirstName LastName        
    -#> * <chr>              <lgl>   <chr>     <chr>           
    -#> 1 0036A00000kHth8QAC NA      Test      Contact-Create-1
    -#> 2 0036A00000kHth9QAC NA      Test      Contact-Create-2
    +

    Update

    After creating records you can update them using sf_update(). Updating a record requires you to pass the Salesforce Id of the record. Salesforce creates a unique 18-character identifier on each record and uses that to know which record to attach the update information you provide. Simply include a field or column in your update dataset called “Id” and the information will be matched. Here is an example where we update each of the records we created earlier with a new first name called “TestTest”.

    -
    # Update some of those records
    -queried_records <- queried_records %>%
    -  mutate(FirstName = "TestTest") %>% 
    -  select(-Account)
    -
    -updated_records <- sf_update(queried_records, object_name="Contact")
    -updated_records
    -#> # A tibble: 2 x 2
    -#>   id                 success
    -#>   <chr>              <chr>  
    -#> 1 0036A00000kHth8QAC true   
    -#> 2 0036A00000kHth9QAC true
    +

    Bulk Operations

    For really large operations (inserts, updates, upserts, deletes, and queries) Salesforce provides the Bulk 1.0 and Bulk 2.0 APIs. In order to use the Bulk APIs in salesforcer you can just add api_type = "Bulk 1.0" or api_type = "Bulk 2.0" to your functions and the operation will be executed using the Bulk APIs. It’s that simple.

    The benefits of using the Bulk API for larger datasets is that the operation will reduce the number of individual API calls (organization usually have a limit on total calls) and batching the requests in bulk is usually quicker than running thousands of individuals calls when your data is large. Note: the Bulk 2.0 API does NOT guarantee the order of the data submitted is preserved in the output. This means that you must join on other data columns to match up the Ids that are returned in the output with the data you submitted. For this reason, Bulk 2.0 may not be a good solution for creating, updating, or upserting records where you need to keep track of the created Ids. The Bulk 2.0 API would be fine for deleting records where you only need to know which Ids were successfully deleted.

    -
    # create contacts using the Bulk API
    -n <- 2
    -new_contacts <- tibble(FirstName = rep("Test", n),
    -                       LastName = paste0("Contact-Create-", 1:n))
    -created_records <- sf_create(new_contacts, object_name="Contact", api_type="Bulk 1.0")
    -
    -# query large recordsets using the Bulk API
    -my_soql <- sprintf("SELECT Id,
    -                           FirstName, 
    -                           LastName
    -                    FROM Contact 
    -                    WHERE Id in ('%s')", 
    -                   paste0(created_records$Id , collapse="','"))
    -
    -queried_records <- sf_query(my_soql, object_name="Contact", api_type="Bulk 1.0")
    -
    -# delete these records using the Bulk API
    -deleted_records <- sf_delete(queried_records$Id, object_name="Contact", api_type="Bulk 1.0")
    +

    Using the Metadata API

    Salesforce is a very flexible platform. Salesforce provides the Metadata API for users to create, read, update and delete the objects, page layouts, and more. This makes it very easy to programmatically setup and teardown the Salesforce environment. One common use case for the Metadata API is retrieving information about an object (fields, permissions, etc.). You can use the sf_read_metadata() function to return a list of objects and their metadata. In the example below we retrieve the metadata for the Account and Contact objects. Note that the metadata_type argument is “CustomObject”. Standard Objects are an implementation of CustomObjects, so they are returned using that metadata type.

    -
    read_obj_result <- sf_read_metadata(metadata_type='CustomObject',
    -                                    object_names=c('Account', 'Contact'))
    -read_obj_result[[1]][c('fullName', 'label', 'sharingModel', 'enableHistory')]
    -#> $fullName
    -#> [1] "Account"
    -#> 
    -#> $label
    -#> [1] "Account"
    -#> 
    -#> $sharingModel
    -#> [1] "ReadWrite"
    -#> 
    -#> $enableHistory
    -#> [1] "false"
    -first_two_fields_idx <- head(which(names(read_obj_result[[1]]) == 'fields'), 2)
    -# show the first two returned fields of the Account object
    -read_obj_result[[1]][first_two_fields_idx]
    -#> $fields
    -#> $fields$fullName
    -#> [1] "AccountNumber"
    -#> 
    -#> $fields$trackFeedHistory
    -#> [1] "false"
    -#> 
    -#> 
    -#> $fields
    -#> $fields$fullName
    -#> [1] "AccountSource"
    -#> 
    -#> $fields$trackFeedHistory
    -#> [1] "false"
    -#> 
    -#> $fields$type
    -#> [1] "Picklist"
    +

    The data is returned as a list because object definitions are highly nested representations. You may notice that we are missing some really specific details, such as, the picklist values of a field with type “Picklist”. You can get that information using the sf_describe_object_fields() or sf_describe_objects() functions which are based on calls from REST and SOAP APIs. Here is an example using sf_describe_object_fields() where we get a tbl_df with one row for each field on the Account object:

    -
    acct_fields <- sf_describe_object_fields('Account')
    -acct_fields %>% select(name, label, length, soapType, type)
    -#> # A tibble: 67 x 5
    -#>    name              label                   length soapType    type     
    -#>    <chr>             <chr>                    <int> <chr>       <chr>    
    -#>  1 Id                Account ID                  18 tns:ID      id       
    -#>  2 IsDeleted         Deleted                      0 xsd:boolean boolean  
    -#>  3 MasterRecordId    Master Record ID            18 tns:ID      reference
    -#>  4 Name              Account Name               255 xsd:string  string   
    -#>  5 Type              Account Type                40 xsd:string  picklist 
    -#>  6 ParentId          Parent Account ID           18 tns:ID      reference
    -#>  7 BillingStreet     Billing Street             255 xsd:string  textarea 
    -#>  8 BillingCity       Billing City                40 xsd:string  string   
    -#>  9 BillingState      Billing State/Province      80 xsd:string  string   
    -#> 10 BillingPostalCode Billing Zip/Postal Code     20 xsd:string  string   
    -#> # ... with 57 more rows
    +

    If you prefer to be more precise about collecting and formatting the field data you can work directly with the nested lists that the APIs return. In this example we look at the picklist values of fields on the Account object.

    -
    describe_obj_result <- sf_describe_objects(object_names=c('Account', 'Contact'))
    -# confirm that the Account object is queryable
    -describe_obj_result[[1]][c('label', 'queryable')]
    -#> $label
    -#> [1] "Account"
    -#> 
    -#> $queryable
    -#> [1] "true"
    -# show the different picklist values for the Account Type field
    -the_type_field <- describe_obj_result[[1]][[59]]
    -the_type_field$label
    -#> NULL
    -map_df(the_type_field[which(names(the_type_field) == "picklistValues")], as_tibble)
    -#> # A tibble: 0 x 0
    +

    It is recommended that you try out the various metadata functions sf_read_metadata(), sf_list_metadata(), sf_describe_metadata(), and sf_describe_objects() in order to see which information best suits your use case.

    Where the Metadata API really shines is when it comes to CRUD operations on metadata. In this example we will create an object, add fields to it, then delete that object.

    -
    # create an object
    -base_obj_name <- "Custom_Account1"
    -custom_object <- list()
    -custom_object$fullName <- paste0(base_obj_name, "__c")
    -custom_object$label <- paste0(gsub("_", " ", base_obj_name))
    -custom_object$pluralLabel <- paste0(base_obj_name, "s")
    -custom_object$nameField <- list(displayFormat = 'AN-{0000}', 
    -                                label = paste0(base_obj_name, ' Number'), 
    -                                type = 'AutoNumber')
    -custom_object$deploymentStatus <- 'Deployed'
    -custom_object$sharingModel <- 'ReadWrite'
    -custom_object$enableActivities <- 'true'
    -custom_object$description <- paste0(base_obj_name, " created by the Metadata API")
    -custom_object_result <- sf_create_metadata(metadata_type = 'CustomObject',
    -                                           metadata = custom_object)
    -
    -# add fields to the object
    -custom_fields <- tibble(fullName=c(paste0(base_obj_name, '__c.CustomField3__c'), 
    -                                   paste0(base_obj_name, '__c.CustomField4__c')), 
    -                        label=c('Test Field3', 'Test Field4'), 
    -                        length=c(100, 100), 
    -                        type=c('Text', 'Text'))
    -create_fields_result <- sf_create_metadata(metadata_type = 'CustomField', 
    -                                           metadata = custom_fields)
    -
    -# delete the object
    -deleted_custom_object_result <- sf_delete_metadata(metadata_type = 'CustomObject', 
    -                                                   object_names = c('Custom_Account1__c'))
    +
    diff --git a/docs/news/index.html b/docs/news/index.html index c38e9335..958be3d4 100644 --- a/docs/news/index.html +++ b/docs/news/index.html @@ -149,9 +149,9 @@

    Changelog

    Source: NEWS.md
    -
    +

    -salesforcer 0.1.2.9000

    +salesforcer 0.1.2.9000

    Features

    @@ -160,9 +160,10 @@

  • Add sf_describe_object_fields() which is a tidyier version of rforcecom.getObjectDescription()
  • -
  • Allow users to control whether bulk query results are kept as all character or the types are guessed (#12)
  • +
  • Allow users to control whether query results are kept as all character or the types are guessed (#12)
  • Add sf_get_all_jobs_bulk() so that users can see retrieve details for all bulk jobs (#13)
  • Add new utility functions sf_set_password() and sf_reset_password() (#11)
  • +
  • Add two new functions to check for duplicates (sf_find_duplicates(), sf_find_duplicates_by_id()) (#4)
  • @@ -171,13 +172,15 @@

    • Fix bug where Username/Password authenticated sessions where not working with api_type = “Bulk 1.0”
    • Fix bug where Bulk 1.0 queries that timeout hit an error while trying to abort since that only supported aborting Bulk 2.0 jobs (#13)
    • +
    • Fix bug that had only production environment logins possible because of hard coding (@weckstm, #18)
    • +
    • Make sf_describe_object_fields() more robust against nested list elements (#16)

    -
    +

    -salesforcer 0.1.2 release +salesforcer 0.1.2 release

    @@ -190,9 +193,9 @@


    -
    +

    -salesforcer 0.1.1 release +salesforcer 0.1.1 release

    @@ -236,9 +239,9 @@


    -
    +

    -salesforcer 0.1.0 release +salesforcer 0.1.0 release

    @@ -286,10 +289,10 @@

    Contents

    diff --git a/docs/pkgdown.yml b/docs/pkgdown.yml index f71b8530..28d0aed6 100644 --- a/docs/pkgdown.yml +++ b/docs/pkgdown.yml @@ -1,4 +1,4 @@ -pandoc: 1.19.2.1 +pandoc: '2.5' pkgdown: 1.1.0 pkgdown_sha: ~ articles: diff --git a/docs/reference/build_soap_xml_from_list.html b/docs/reference/build_soap_xml_from_list.html index 75c1414c..6d0496d0 100644 --- a/docs/reference/build_soap_xml_from_list.html +++ b/docs/reference/build_soap_xml_from_list.html @@ -160,9 +160,10 @@

    Build XML Request Body

    build_soap_xml_from_list(input_data, operation = c("create", "retrieve",
       "update", "upsert", "delete", "search", "query", "queryMore",
    -  "describeSObjects", "setPassword", "resetPassword"),
    -  object_name = NULL, fields = NULL, external_id_fieldname = NULL,
    -  root_name = NULL, ns = c(character(0)), root = NULL)
    + "describeSObjects", "setPassword", "resetPassword", "findDuplicates", + "findDuplicatesByIds"), object_name = NULL, fields = NULL, + external_id_fieldname = NULL, root_name = NULL, + ns = c(character(0)), root = NULL)

    Arguments

    diff --git a/docs/reference/collapse_list_with_dupe_names.html b/docs/reference/collapse_list_with_dupe_names.html new file mode 100644 index 00000000..a09ae855 --- /dev/null +++ b/docs/reference/collapse_list_with_dupe_names.html @@ -0,0 +1,241 @@ + + + + + + + + +Collapse Elements in List with Same Name — collapse_list_with_dupe_names • salesforcer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + +
    + +
    +
    + + +
    + +

    This function looks for instances of elements in a list that have the same name +and then combine them all into a single comma separated character string (referenceTo) +or tbl_df (picklistValues).

    + +
    + +
    collapse_list_with_dupe_names(x)
    + +

    Arguments

    +
    + + + + + +
    x

    list; a list, typically returned from the API that we would parse through

    + +

    Value

    + +

    A list containing one row per field for the requested object.

    + +

    Note

    + +

    The tibble only contains the fields that the user can view, as defined by +the user's field-level security settings.

    + + +

    Examples

    +
    # NOT RUN {
    +obj_dat <- sf_describe_objects(object_names = object_name, api_type = "SOAP")[[1]]
    +obj_fields_list <- obj_dat[names(obj_dat) == "fields"] %>%
    +  map(collapse_list_with_dupe_names)
    +# }
    +
    + +
    + +
    + + +
    +

    Site built with pkgdown.

    +
    + +
    +
    + + + + + + + + + diff --git a/docs/reference/sf_find_duplicates.html b/docs/reference/sf_find_duplicates.html new file mode 100644 index 00000000..337e4543 --- /dev/null +++ b/docs/reference/sf_find_duplicates.html @@ -0,0 +1,260 @@ + + + + + + + + +Find Duplicate Records — sf_find_duplicates • salesforcer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + +
    + +
    +
    + + +
    + +

    Performs rule-based searches for duplicate records.

    + +
    + +
    sf_find_duplicates(search_criteria, object_name,
    +  include_record_details = FALSE, verbose = FALSE)
    + +

    Arguments

    + + + + + + + + + + + + + + + + + + +
    search_criteria

    list; a list of fields and their values that would +constitute a match. For example, list(FirstName="Marc", Company="Salesforce")

    object_name

    character; the name of one Salesforce objects that the +function is operating against (e.g. "Account", "Contact", "CustomObject__c")

    include_record_details

    logical; get fields and values for records detected +as duplicates by setting this property to TRUE. Get only record IDs for +records detected as duplicates by setting this property to FALSE

    verbose

    logical; do you want informative messages?

    + +

    Value

    + +

    tbl_df of records found to be duplicates by the match rules

    + +

    Note

    + +

    You must have actived duplicate rules for the supplied object before running +this function. The object_name argument refers to using that object's duplicate +rules on the search criteria to determine which records in other objects are duplicates.

    + + +

    Examples

    +
    # NOT RUN {
    +# if insert a lead with this email address, what duplicate records exist elsewhere 
    +# according to the Lead object's duplicate rules
    +found_dupes <- sf_find_duplicates(search_criteria = list(Email = "bond_john@grandhotels.com"),
    +                                  object_name = "Lead")
    +
    +# now look across all other objects using the Contact object rules
    +found_dupes <- sf_find_duplicates(search_criteria = list(Email = "bond_john@grandhotels.com"),
    +                                  object_name = "Contact")
    +# }
    +
    + +
    + +
    + + +
    +

    Site built with pkgdown.

    +
    + +
    +
    + + + + + + + + + diff --git a/docs/reference/sf_find_duplicates_by_id.html b/docs/reference/sf_find_duplicates_by_id.html new file mode 100644 index 00000000..22e210ef --- /dev/null +++ b/docs/reference/sf_find_duplicates_by_id.html @@ -0,0 +1,249 @@ + + + + + + + + +Find Duplicate Records By Id — sf_find_duplicates_by_id • salesforcer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + +
    + +
    +
    + + +
    + +

    Performs rule-based searches for duplicate records.

    + +
    + +
    sf_find_duplicates_by_id(sf_id, include_record_details = FALSE,
    +  verbose = FALSE)
    + +

    Arguments

    + + + + + + + + + + + + + + +
    sf_id

    character; a Salesforce generated Id that identifies a record

    include_record_details

    logical; get fields and values for records detected +as duplicates by setting this property to TRUE. Get only record IDs for +records detected as duplicates by setting this property to FALSE

    verbose

    logical; do you want informative messages?

    + +

    Value

    + +

    tbl_df of records found to be duplicates by the match rules

    + +

    Note

    + +

    You must have actived duplicate rules for the supplied object before running +this function. This function uses the duplicate rules for the object that has +the same type as the input record IDs. For example, if the record Id represents +an Account, this function uses the duplicate rules associated with the +Account object.

    + + +

    Examples

    +
    # NOT RUN {
    +found_dupes <- sf_find_duplicates_by_id(sf_id = "00Q6A00000aABCnZZZ")
    +# }
    +
    + +
    + +
    + + +
    +

    Site built with pkgdown.

    +
    + +
    +
    + + + + + + + + + diff --git a/docs/reference/sf_query.html b/docs/reference/sf_query.html index 81c24d65..fa2fbb90 100644 --- a/docs/reference/sf_query.html +++ b/docs/reference/sf_query.html @@ -160,9 +160,9 @@

    Perform SOQL Query

    -
    sf_query(soql, object_name, queryall = FALSE, page_size = 1000,
    -  api_type = c("REST", "SOAP", "Bulk 1.0"), next_records_url = NULL,
    -  ..., verbose = FALSE)
    +
    sf_query(soql, object_name, guess_types = TRUE, queryall = FALSE,
    +  page_size = 1000, api_type = c("REST", "SOAP", "Bulk 1.0"),
    +  next_records_url = NULL, ..., verbose = FALSE)

    Arguments

    @@ -175,6 +175,12 @@

    Arg

    + + + + diff --git a/docs/sitemap.xml b/docs/sitemap.xml index 4606f730..55029c6b 100644 --- a/docs/sitemap.xml +++ b/docs/sitemap.xml @@ -15,6 +15,9 @@ https://stevenmmortimer.github.io/salesforcer/reference/catch_errors.html + + https://stevenmmortimer.github.io/salesforcer/reference/collapse_list_with_dupe_names.html + https://stevenmmortimer.github.io/salesforcer/reference/get_os.html @@ -234,6 +237,12 @@ https://stevenmmortimer.github.io/salesforcer/reference/sf_end_job_bulk.html + + https://stevenmmortimer.github.io/salesforcer/reference/sf_find_duplicates.html + + + https://stevenmmortimer.github.io/salesforcer/reference/sf_find_duplicates_by_id.html + https://stevenmmortimer.github.io/salesforcer/reference/sf_get_all_jobs_bulk.html diff --git a/index.Rmd b/index.Rmd index c69634b5..77ee2d96 100644 --- a/index.Rmd +++ b/index.Rmd @@ -22,7 +22,9 @@ knitr::opts_chunk$set( **salesforcer** is an R package that connects to Salesforce Platform APIs using tidy principles. The package implements most actions from the SOAP, REST, Bulk 1.0, -Bulk 2.0, and Metadata APIs. Package features include: +Bulk 2.0, and Metadata APIs. + +Package features include: * OAuth 2.0 (Single Sign On) and Basic (Username-Password) Authentication methods (`sf_auth()`) * CRUD (Create, Retrieve, Update, Delete) methods for records using the SOAP, REST, and Bulk APIs diff --git a/man-roxygen/include_record_details.R b/man-roxygen/include_record_details.R new file mode 100644 index 00000000..e1f33d0e --- /dev/null +++ b/man-roxygen/include_record_details.R @@ -0,0 +1,3 @@ +#' @param include_record_details logical; get fields and values for records detected +#' as duplicates by setting this property to \code{TRUE}. Get only record IDs for +#' records detected as duplicates by setting this property to \code{FALSE} diff --git a/man-roxygen/sf_id.R b/man-roxygen/sf_id.R new file mode 100644 index 00000000..27ee795b --- /dev/null +++ b/man-roxygen/sf_id.R @@ -0,0 +1 @@ +#' @param sf_id character; a Salesforce generated Id that identifies a record diff --git a/man/build_soap_xml_from_list.Rd b/man/build_soap_xml_from_list.Rd index 81ee0463..fb011dd2 100644 --- a/man/build_soap_xml_from_list.Rd +++ b/man/build_soap_xml_from_list.Rd @@ -6,9 +6,10 @@ \usage{ build_soap_xml_from_list(input_data, operation = c("create", "retrieve", "update", "upsert", "delete", "search", "query", "queryMore", - "describeSObjects", "setPassword", "resetPassword"), - object_name = NULL, fields = NULL, external_id_fieldname = NULL, - root_name = NULL, ns = c(character(0)), root = NULL) + "describeSObjects", "setPassword", "resetPassword", "findDuplicates", + "findDuplicatesByIds"), object_name = NULL, fields = NULL, + external_id_fieldname = NULL, root_name = NULL, + ns = c(character(0)), root = NULL) } \arguments{ \item{input_data}{a \code{data.frame} of data to fill the XML body} diff --git a/man/sf_find_duplicates.Rd b/man/sf_find_duplicates.Rd new file mode 100644 index 00000000..c66bbaec --- /dev/null +++ b/man/sf_find_duplicates.Rd @@ -0,0 +1,45 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils-org.R +\name{sf_find_duplicates} +\alias{sf_find_duplicates} +\title{Find Duplicate Records} +\usage{ +sf_find_duplicates(search_criteria, object_name, + include_record_details = FALSE, verbose = FALSE) +} +\arguments{ +\item{search_criteria}{\code{list}; a list of fields and their values that would +constitute a match. For example, list(FirstName="Marc", Company="Salesforce")} + +\item{object_name}{character; the name of one Salesforce objects that the +function is operating against (e.g. "Account", "Contact", "CustomObject__c")} + +\item{include_record_details}{logical; get fields and values for records detected +as duplicates by setting this property to \code{TRUE}. Get only record IDs for +records detected as duplicates by setting this property to \code{FALSE}} + +\item{verbose}{logical; do you want informative messages?} +} +\value{ +\code{tbl_df} of records found to be duplicates by the match rules +} +\description{ +Performs rule-based searches for duplicate records. +} +\note{ +You must have actived duplicate rules for the supplied object before running +this function. The \code{object_name} argument refers to using that object's duplicate +rules on the search criteria to determine which records in other objects are duplicates. +} +\examples{ +\dontrun{ +# if insert a lead with this email address, what duplicate records exist elsewhere +# according to the Lead object's duplicate rules +found_dupes <- sf_find_duplicates(search_criteria = list(Email = "bond_john@grandhotels.com"), + object_name = "Lead") + +# now look across all other objects using the Contact object rules +found_dupes <- sf_find_duplicates(search_criteria = list(Email = "bond_john@grandhotels.com"), + object_name = "Contact") +} +} diff --git a/man/sf_find_duplicates_by_id.Rd b/man/sf_find_duplicates_by_id.Rd new file mode 100644 index 00000000..069bb66c --- /dev/null +++ b/man/sf_find_duplicates_by_id.Rd @@ -0,0 +1,36 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils-org.R +\name{sf_find_duplicates_by_id} +\alias{sf_find_duplicates_by_id} +\title{Find Duplicate Records By Id} +\usage{ +sf_find_duplicates_by_id(sf_id, include_record_details = FALSE, + verbose = FALSE) +} +\arguments{ +\item{sf_id}{character; a Salesforce generated Id that identifies a record} + +\item{include_record_details}{logical; get fields and values for records detected +as duplicates by setting this property to \code{TRUE}. Get only record IDs for +records detected as duplicates by setting this property to \code{FALSE}} + +\item{verbose}{logical; do you want informative messages?} +} +\value{ +\code{tbl_df} of records found to be duplicates by the match rules +} +\description{ +Performs rule-based searches for duplicate records. +} +\note{ +You must have actived duplicate rules for the supplied object before running +this function. This function uses the duplicate rules for the object that has +the same type as the input record IDs. For example, if the record Id represents +an Account, this function uses the duplicate rules associated with the +Account object. +} +\examples{ +\dontrun{ +found_dupes <- sf_find_duplicates_by_id(sf_id = "00Q6A00000aABCnZZZ") +} +} diff --git a/tests/testthat/test-metadata.R b/tests/testthat/test-metadata.R index 27793ec7..98294c5d 100644 --- a/tests/testthat/test-metadata.R +++ b/tests/testthat/test-metadata.R @@ -140,9 +140,8 @@ test_that("sf_read_metadata", { test_that("sf_describe_object_fields", { expect_is(desc_obj_fields_result, "tbl_df") - expect_true(all(c('Id', 'OwnerId', 'IsDeleted', 'Name', 'CreatedDate', 'CreatedById', - 'LastModifiedDate', 'LastModifiedById', 'SystemModstamp', 'LastActivityDate') %in% - desc_obj_fields_result$name)) + expect_true(all(c('name', 'label', 'length', 'custom', + 'type', 'updateable') %in% names(desc_obj_fields_result))) }) test_that("sf_retrieve_metadata", { diff --git a/tests/testthat/test-org-utils.R b/tests/testthat/test-org-utils.R index 968c3332..9a1f6cbb 100644 --- a/tests/testthat/test-org-utils.R +++ b/tests/testthat/test-org-utils.R @@ -52,3 +52,16 @@ test_that("testing sf_list_objects()", { expect_true(all(c("Account", "Contact", "Lead", "Opportunity", "Task") %in% valid_object_names)) }) + +test_that("testing sf_find_duplicates()", { + duplicates_search <- sf_find_duplicates(search_criteria = list(Email="bond_john@grandhotels.com"), + object_name = "Contact") + expect_is(duplicates_search, "tbl_df") + expect_named(duplicates_search, c("type", "Id")) +}) + +test_that("testing sf_find_duplicates_by_id()", { + duplicates_search <- sf_find_duplicates_by_id(sf_id = "0036A000002C6McQAK") + expect_is(duplicates_search, "tbl_df") + expect_true(all(c("type", "Id") %in% names(duplicates_search))) +}) diff --git a/vignettes/.gitignore b/vignettes/.gitignore deleted file mode 100644 index 21e95bb7..00000000 --- a/vignettes/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.httr-oauth-salesforcer
    object_name

    character; the name of one Salesforce objects that the function is operating against (e.g. "Account", "Contact", "CustomObject__c")

    guess_types

    logical; indicating whether or not to use col_guess() +to try and cast the data returned in the query recordset. TRUE uses col_guess() +and FALSE returns all values as character strings.

    queryall