diff --git a/.github/workflows/main-01-pkgdown.yml b/.github/workflows/main-01-pkgdown.yml index 83d6a500..5aa78c87 100644 --- a/.github/workflows/main-01-pkgdown.yml +++ b/.github/workflows/main-01-pkgdown.yml @@ -1,6 +1,12 @@ name: Build pkgdown site on: + workflow_dispatch: + inputs: + trigger_next: + description: 'Whether to run the subsequent workflows after triggering this one manully.' + required: false + default: false push: branches: main @@ -74,7 +80,7 @@ jobs: Rscript -e 'pkgdown::deploy_to_branch(new_process = FALSE)' - name: Trigger next workflow - if: success() + if: ${{ (github.event_name != 'workflow_dispatch' && success()) || (github.event_name == 'workflow_dispatch' && github.event.inputs.trigger_next && success()) }} uses: peter-evans/repository-dispatch@v1 with: token: ${{ secrets.REPO_GHA_PAT }} @@ -83,7 +89,7 @@ jobs: client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}"}' - name: Set final R-CMD-check status - if: failure() + if: ${{ (github.event_name != 'workflow_dispatch' && failure()) }} uses: peter-evans/repository-dispatch@v1 with: token: ${{ secrets.REPO_GHA_PAT }} diff --git a/.github/workflows/main-02-test-coverage.yml b/.github/workflows/main-02-test-coverage.yml index 0734ca8e..77e3b9cf 100644 --- a/.github/workflows/main-02-test-coverage.yml +++ b/.github/workflows/main-02-test-coverage.yml @@ -1,11 +1,17 @@ name: Test coverage on: + workflow_dispatch: + inputs: + trigger_next: + description: 'Whether to run the subsequent workflows after triggering this one manully.' + required: false + default: false repository_dispatch: types: [main-02-test-coverage] schedule: - # Execute monthly at 9AM UTC (5AM EDT (during daylight savings), otherwise 4AM) - - cron: '0 9 1 * *' + # Execute monthly at 9AM UTC (5AM ET during daylight savings, otherwise 4AM) + - cron: '0 9 1 * *' jobs: test-coverage: @@ -72,7 +78,7 @@ jobs: shell: Rscript {0} - name: Trigger next workflow - if: success() + if: ${{ (github.event_name != 'workflow_dispatch' && success()) || (github.event_name == 'workflow_dispatch' && github.event.inputs.trigger_next && success()) }} uses: peter-evans/repository-dispatch@v1 with: token: ${{ secrets.REPO_GHA_PAT }} @@ -81,7 +87,7 @@ jobs: client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}"}' - name: Set final R-CMD-check status - if: failure() + if: ${{ (github.event_name != 'workflow_dispatch' && failure()) }} uses: peter-evans/repository-dispatch@v1 with: token: ${{ secrets.REPO_GHA_PAT }} diff --git a/.github/workflows/main-03-R-CMD-check-mac.yml b/.github/workflows/main-03-R-CMD-check-mac.yml index 9b923eae..798cc09f 100644 --- a/.github/workflows/main-03-R-CMD-check-mac.yml +++ b/.github/workflows/main-03-R-CMD-check-mac.yml @@ -1,6 +1,12 @@ name: R-CMD-check on Mac on: + workflow_dispatch: + inputs: + trigger_next: + description: 'Whether to run the subsequent workflows after triggering this one manully.' + required: false + default: false repository_dispatch: types: [main-03-R-CMD-check-mac] @@ -92,7 +98,7 @@ jobs: path: check - name: Trigger next workflow - if: success() + if: ${{ (github.event_name != 'workflow_dispatch' && success()) || (github.event_name == 'workflow_dispatch' && github.event.inputs.trigger_next && success()) }} uses: peter-evans/repository-dispatch@v1 with: token: ${{ secrets.REPO_GHA_PAT }} @@ -101,7 +107,7 @@ jobs: client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}"}' - name: Set final R-CMD-check status - if: failure() + if: ${{ (github.event_name != 'workflow_dispatch' && failure()) }} uses: peter-evans/repository-dispatch@v1 with: token: ${{ secrets.REPO_GHA_PAT }} diff --git a/.github/workflows/main-04-R-CMD-check-windows.yml b/.github/workflows/main-04-R-CMD-check-windows.yml index 0e06c53c..55bce1e9 100644 --- a/.github/workflows/main-04-R-CMD-check-windows.yml +++ b/.github/workflows/main-04-R-CMD-check-windows.yml @@ -1,6 +1,12 @@ name: R-CMD-check on Windows on: + workflow_dispatch: + inputs: + trigger_next: + description: 'Whether to run the subsequent workflows after triggering this one manully.' + required: false + default: false repository_dispatch: types: [main-04-R-CMD-check-windows] @@ -92,7 +98,7 @@ jobs: path: check - name: Trigger next workflow - if: success() + if: ${{ (github.event_name != 'workflow_dispatch' && success()) || (github.event_name == 'workflow_dispatch' && github.event.inputs.trigger_next && success()) }} uses: peter-evans/repository-dispatch@v1 with: token: ${{ secrets.REPO_GHA_PAT }} @@ -101,7 +107,7 @@ jobs: client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}"}' - name: Set final R-CMD-check status - if: failure() + if: ${{ (github.event_name != 'workflow_dispatch' && failure()) }} uses: peter-evans/repository-dispatch@v1 with: token: ${{ secrets.REPO_GHA_PAT }} diff --git a/.github/workflows/main-05-R-CMD-check-linux.yml b/.github/workflows/main-05-R-CMD-check-linux.yml index 38d55147..3cac6b7e 100644 --- a/.github/workflows/main-05-R-CMD-check-linux.yml +++ b/.github/workflows/main-05-R-CMD-check-linux.yml @@ -1,6 +1,12 @@ name: R-CMD-check on Linux on: + workflow_dispatch: + inputs: + trigger_next: + description: 'Whether to run the subsequent workflows after triggering this one manully.' + required: false + default: false repository_dispatch: types: [main-05-R-CMD-check-linux] @@ -92,7 +98,7 @@ jobs: path: check - name: Set final R-CMD-check status - if: always() + if: ${{ (github.event_name != 'workflow_dispatch') || (github.event_name == 'workflow_dispatch' && github.event.inputs.trigger_next) }} uses: peter-evans/repository-dispatch@v1 with: token: ${{ secrets.REPO_GHA_PAT }} diff --git a/.github/workflows/main-06-R-CMD-check-final.yml b/.github/workflows/main-06-R-CMD-check-final.yml index 2720acdc..3f52a167 100644 --- a/.github/workflows/main-06-R-CMD-check-final.yml +++ b/.github/workflows/main-06-R-CMD-check-final.yml @@ -1,6 +1,12 @@ name: R-CMD-check on: + workflow_dispatch: + inputs: + force_failure: + description: 'A backdoor trigger to force the final R CMD Check status to failed.' + required: true + default: false repository_dispatch: types: [main-06-R-CMD-check-final] @@ -12,5 +18,6 @@ jobs: steps: - name: Convey final status of all R-CMD-check workflows - if: ${{ !github.event.client_payload.success }} + if: ${{ (github.event_name != 'workflow_dispatch' && !github.event.client_payload.success) || (github.event_name == 'workflow_dispatch' && github.event.inputs.force_failure) }} run: exit 1 + diff --git a/.gitignore b/.gitignore index 82e1c4e7..5616166d 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ # produced vignettes vignettes/*.html +vignettes/*.md vignettes/*.pdf vignettes/*.R @@ -41,8 +42,8 @@ salesforcer.tar .DS_Store # Folders created by vignette builds -doc/ -Meta/ +/doc/ +/Meta/ # Files created by tests *.pdf diff --git a/DESCRIPTION b/DESCRIPTION index 40a279e9..14948f7f 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: salesforcer Title: An Implementation of 'Salesforce' APIs Using Tidy Principles -Version: 0.2.2.9000 -Date: 2020-09-12 +Version: 1.0.0 +Date: 2021-07-04 Description: Functions connecting to the 'Salesforce' Platform APIs (REST, SOAP, Bulk 1.0, Bulk 2.0, Metadata, Reports and Dashboards) . @@ -11,12 +11,23 @@ Description: Functions connecting to the 'Salesforce' Platform APIs (REST, SOAP, API documentation and this package's website for more information, documentation, and examples. -Authors@R: c( - person(c("Steven", "M."), "Mortimer", , "mortimer.steven.m@gmail.com", c("aut", "cre")), - person("Takekatsu", "Hiramura", , "thira@plavox.info", c("ctb")), - person("Jennifer", "Bryan", , "jenny@rstudio.com", c("ctb", "cph")), - person("Joanna", "Zhao", , "joanna.zhao@alumni.ubc.ca", c("ctb", "cph")) - ) +Authors@R: + c(person(given = c("Steven", "M."), + family = "Mortimer", + role = c("aut", "cre"), + email = "mortimer.steven.m@gmail.com"), + person(given = "Takekatsu", + family = "Hiramura", + role = "ctb", + email = "thira@plavox.info"), + person(given = "Jennifer", + family = "Bryan", + role = c("ctb", "cph"), + email = "jenny@rstudio.com"), + person(given = "Joanna", + family = "Zhao", + role = c("ctb", "cph"), + email = "joanna.zhao@alumni.ubc.ca")) License: MIT + file LICENSE URL: https://github.com/StevenMMortimer/salesforcer BugReports: https://github.com/StevenMMortimer/salesforcer/issues @@ -32,6 +43,7 @@ Imports: tibble (>= 3.0.3), readr (>= 1.3.1), lubridate (>= 1.7.8), + anytime (>= 0.3.9), rlang (>= 0.4.7), httr (>= 1.4.1), curl (>= 4.3), @@ -46,19 +58,16 @@ Imports: lifecycle (>= 0.2.0) Suggests: knitr, + rmarkdown, testthat, spelling, - rmarkdown, here, microbenchmark, ggplot2, - sessioninfo, - RForcecom -VignetteBuilder: - knitr + sessioninfo +VignetteBuilder: knitr ByteCompile: true Encoding: UTF-8 Language: en-US -LazyData: true Roxygen: list(markdown = TRUE) RoxygenNote: 7.1.1 diff --git a/NAMESPACE b/NAMESPACE index e48f0f02..1c22e0ef 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -21,6 +21,7 @@ export(build_proxy) export(build_soap_xml_from_list) export(catch_errors) export(catch_unknown_api) +export(check_and_encode_files) export(collapse_list_with_dupe_names) export(combine_parent_and_child_resultsets) export(compact2) @@ -38,6 +39,7 @@ export(format_headers_for_verbose) export(format_report_row) export(get_os) export(guess_object_name_from_soql) +export(is_legit_token) export(list_extract_parent_and_child_result) export(make_analytics_folder_child_operations_url) export(make_analytics_folder_collections_url) @@ -151,6 +153,8 @@ export(sf_create) export(sf_create_attachment) export(sf_create_batches_bulk) export(sf_create_job_bulk) +export(sf_create_job_bulk_v1) +export(sf_create_job_bulk_v2) export(sf_create_metadata) export(sf_create_report) export(sf_dashboard_components_describe) @@ -184,6 +188,7 @@ export(sf_find_duplicates) export(sf_find_duplicates_by_id) export(sf_format_date) export(sf_format_datetime) +export(sf_format_time) export(sf_get_all_jobs_bulk) export(sf_get_all_query_jobs_bulk) export(sf_get_dashboard_data) @@ -282,6 +287,8 @@ importFrom(XML,xmlSApply) importFrom(XML,xmlSize) importFrom(XML,xmlToList) importFrom(XML,xmlValue) +importFrom(anytime,anydate) +importFrom(anytime,anytime) importFrom(base64enc,base64encode) importFrom(curl,form_data) importFrom(curl,form_file) @@ -308,7 +315,11 @@ importFrom(dplyr,rename_at) importFrom(dplyr,rename_with) importFrom(dplyr,select) importFrom(dplyr,tibble) +importFrom(httr,DELETE) importFrom(httr,GET) +importFrom(httr,PATCH) +importFrom(httr,POST) +importFrom(httr,PUT) importFrom(httr,RETRY) importFrom(httr,add_headers) importFrom(httr,build_url) @@ -359,6 +370,7 @@ importFrom(purrr,transpose) importFrom(readr,col_character) importFrom(readr,col_guess) importFrom(readr,cols) +importFrom(readr,locale) importFrom(readr,parse_datetime) importFrom(readr,read_csv) importFrom(readr,type_convert) diff --git a/NEWS.md b/NEWS.md index 3733cbcb..c5659896 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,16 +1,51 @@ -## salesforcer 0.2.2.9000 +## salesforcer 1.0.0 [release](https://github.com/StevenMMortimer/salesforcer/releases/tag/v1.0.0) ### Dependencies - * None yet. + * Increase the package's default Salesforce API version to 52.0 (Summer '21). + + * Remove uses of {RForcecom} after it was archived on CRAN on 6/8/2021 (#101) + + * Remove LazyData option in DESCRIPTION since `data()` is not utilized + + * Deprecate argument in `sf_write_csv()` from `path` to `file` as was done in + {readr} v1.4.0. + + * Deprecate argument `bind_using_character_cols` because we will always need + to bind as character and then parse if `guess_types=TRUE`. Per comments in + tidyverse/readr#588 and tidyverse/readr#98, we must read all of the data as + character first and then use `type_convert()` to ensure that we use all values + in the column to guess the type. The default for `read_csv()` is to only use + the first 1,000 rows and its `guess_max` argument cannot be set to `Inf`. + + * Change lifecycle status from "Maturing" to "Status" per the retirement of + "Maturing" in the {lifecycle} package. The documentation notes: + + > Previously we used as maturing for functions that lay somewhere between experimental and stable. We stopped using this stage because, like questioning, it’s not clear what actionable information this stage delivers. + + In addition, the lifecycle guidance states that experimental packages have + version numbers less than 1.0.0 and may have major changes in its future. + The {salesforcer} package has achieved a stable state with core + functionality implemented and a focus on backwards compatibility due to the + volume of users. ### Features - * None yet. + * Improve documentation to retrieve the access token or session ID after + authentication (#97) + + * Improve parsing of Bulk API query recordsets from CSV where all values + in the column will be used to guess the type instead of the first 1000. ### Bug fixes - * None yet. + * Generalize the date and datetime parsing mechanism, such that, reports with + date and datetime fields are not returned as NA (#93) + + * Fix the format of the `OwnerChangeOptions` header so it is accepted (#94) + + * Fix bug that caused Bulk 2.0 calls to crash when the results had datetime + fields in the recordset (#95) --- diff --git a/R/analytics-report.R b/R/analytics-report.R index 408c8d8e..cc79c3fa 100644 --- a/R/analytics-report.R +++ b/R/analytics-report.R @@ -621,6 +621,7 @@ sf_delete_report <- function(report_id, verbose=FALSE){ #' \item reportFilters #' } #' +#' @importFrom lifecycle deprecated is_present deprecate_warn #' @importFrom dplyr mutate across select any_of everything #' @importFrom readr parse_datetime type_convert cols col_guess #' @importFrom tibble as_tibble_row @@ -686,11 +687,21 @@ sf_execute_report <- function(report_id, include_details = TRUE, labels = TRUE, guess_types = TRUE, - bind_using_character_cols = FALSE, + bind_using_character_cols = deprecated(), as_tbl = TRUE, report_metadata = NULL, verbose = FALSE){ + if(is_present(bind_using_character_cols)) { + deprecate_warn("1.0.0", + "salesforcer::sf_execute_report(bind_using_character_cols)", + details = paste0("The `bind_using_character_cols` functionality ", + "will always be `TRUE` going forward. Per the ", + "{readr} package, we have to read as character ", + "and then invoke `type_convert()` in order to ", + "use all values in a column to guess its type.")) + } + if(!is.null(report_metadata)){ report_metadata <- sf_input_data_validation(report_metadata, operation = "filter_report") @@ -736,8 +747,7 @@ sf_execute_report <- function(report_id, response_parsed <- response_parsed %>% parse_report_detail_rows( labels = labels, - guess_types = guess_types, - bind_using_character_cols = bind_using_character_cols + guess_types = guess_types ) } } @@ -859,6 +869,7 @@ sf_delete_report_instance <- function(report_id, #' filters. Depending on your asynchronous report run request, data can be at the #' summary level or include details. #' +#' @importFrom lifecycle deprecated is_present deprecate_warn #' @importFrom purrr map_df pluck set_names map_chr #' @template report_id #' @template report_instance_id @@ -898,18 +909,27 @@ sf_get_report_instance_results <- function(report_id, report_instance_id, labels = TRUE, guess_types = TRUE, - bind_using_character_cols = FALSE, + bind_using_character_cols = deprecated(), fact_map_key = "T!T", verbose = FALSE){ + if(is_present(bind_using_character_cols)) { + deprecate_warn("1.0.0", + "salesforcer::sf_get_report_instance_results(bind_using_character_cols)", + details = paste0("The `bind_using_character_cols` functionality ", + "will always be `TRUE` going forward. Per the ", + "{readr} package, we have to read as character ", + "and then invoke `type_convert()` in order to ", + "use all values in a column to guess its type.")) + } + this_url <- make_report_instance_url(report_id, report_instance_id) resultset <- sf_rest_list(url = this_url, as_tbl = FALSE, verbose = verbose) resultset <- resultset %>% parse_report_detail_rows( fact_map_key = fact_map_key, labels = labels, - guess_types = guess_types, - bind_using_character_cols = bind_using_character_cols + guess_types = guess_types ) return(resultset) } @@ -932,6 +952,7 @@ sf_get_report_instance_results <- function(report_id, #' function arguments rather than forcing the user to create an entire list of #' \code{reportMetadata}. #' +#' @importFrom lifecycle deprecated is_present deprecate_warn #' @template report_id #' @param report_filters \code{list}; A \code{list} of reportFilter specifications. #' Each must be a list with 3 elements: 1) \code{column}, 2) \code{operator}, and @@ -962,6 +983,9 @@ sf_get_report_instance_results <- function(report_id, #' report finish running so that data can be obtained. Otherwise, return the #' report instance details which can be used to retrieve the results when the #' async report has finished. +#' @template guess_types +#' @template bind_using_character_cols +#' @template fact_map_key #' @template verbose #' @return \code{tbl_df} #' @family Report functions @@ -1017,8 +1041,21 @@ sf_run_report <- function(report_id, interval_seconds = 3, max_attempts = 200, wait_for_results = TRUE, + guess_types = TRUE, + bind_using_character_cols = deprecated(), + fact_map_key = "T!T", verbose = FALSE){ + if(is_present(bind_using_character_cols)) { + deprecate_warn("1.0.0", + "salesforcer::sf_run_report(bind_using_character_cols)", + details = paste0("The `bind_using_character_cols` functionality ", + "will always be `TRUE` going forward. Per the ", + "{readr} package, we have to read as character ", + "and then invoke `type_convert()` in order to ", + "use all values in a column to guess its type.")) + } + # build out the body of the request based on the inputted arguments by starting # with a simplified version and then adding to it based on the user inputted arguments request_body <- simplify_report_metadata(report_id, verbose = verbose) @@ -1090,7 +1127,8 @@ sf_run_report <- function(report_id, results <- sf_execute_report(report_id, async = async, - report_metadata = request_body, + report_metadata = request_body, + guess_types = guess_types, verbose = verbose) # request the report results (still wait if async is specified) @@ -1123,6 +1161,8 @@ sf_run_report <- function(report_id, } results <- sf_get_report_instance_results(report_id, results$id, + guess_types = guess_types, + fact_map_key = "T!T", verbose = verbose) } } diff --git a/R/attachments.R b/R/attachments.R index 52019d5b..82345749 100644 --- a/R/attachments.R +++ b/R/attachments.R @@ -1,7 +1,7 @@ #' Download an Attachment #' #' @description -#' `r lifecycle::badge("maturing")` +#' `r lifecycle::badge("stable")` #' #' This function will allow you to download an attachment to disk based on the #' attachment body, file name, and path. @@ -635,8 +635,20 @@ sf_delete_attachment <- function(ids, #' #' @importFrom base64enc base64encode #' @family Attachment functions +#' @param dat \code{tbl_df} or \code{list} of information regarding attachments +#' stored locally that will be encoded for use in the APIs. +#' @param column \code{character}; a string that indicates which column in the +#' \code{dat} argument is storing the body of information that needs to be encoded. +#' @param encode \code{logical}; a indicator of whether the body column should +#' be encoded in this step, which allows us to utilize this function for checking +#' or checking and encoding. +#' @param n_check \code{integer}; an integer specifying how many elements in the +#' \code{dat} argument that should be checked to see if the referenced file path +#' exists locally. This fails the function early if users accidentally specify +#' the wrong path. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal +#' @export check_and_encode_files <- function(dat, column = "Body", encode = TRUE, n_check = 100){ # Documents can be created from Urls so ignore encoding if there is no "Body" column if(column == "Body" & !("Body" %in% names(dat))){ diff --git a/R/auth.R b/R/auth.R index 8ee3091c..419abd04 100644 --- a/R/auth.R +++ b/R/auth.R @@ -34,7 +34,7 @@ #' @description #' `r lifecycle::badge("stable")` #' -#' Log in using Basic (Username-Password) or OAuth 2.0 authenticaion. OAuth does +#' Log in using Basic (Username-Password) or OAuth 2.0 authentication. OAuth does #' not require sharing passwords, but will require authorizing \code{salesforcer} #' as a connected app to view and manage your organization. You will be directed to #' a web browser, asked to sign in to your Salesforce account, and to grant \code{salesforcer} @@ -59,6 +59,32 @@ #' the default cache file \code{.httr-oauth-salesforcer}, FALSE means do not #' cache. A string means use the specified path as the cache file. #' @template verbose +#' @return \code{list} invisibly that contains 4 elements detailing the authentication state +#' @note The \code{link{sf_auth}} function invisibly returns the following +#' 4 pieces of information which can be reused in other operations: +#' \describe{ +#' \item{auth_method}{ +#' \code{character}; One of two options 'Basic' or 'OAuth'. If a username, +#' password, and security token were supplied, then this would result in +#' 'Basic' authentication. +#' } +#' \item{token}{ +#' \code{Token2.0}; The object returned by \code{\link[httr]{oauth2.0_token}}. +#' This value is \code{NULL} if \code{auth_method='Basic'}. +#' } +#' \item{session_id}{ +#' \code{character}; A unique ID associated with this user session. The session +#' ID is obtained from the X-SFDC-Session header fetched with SOAP API's login() +#' call. This value is \code{NULL} if \code{auth_method='OAuth'}. +#' } +#' \item{instance_url}{ +#' \code{character}; The domain address of the server that your Salesforce org +#' is on and where subsequent API calls will be directed to. For example, +#' \code{https://na21.salesforce.com} refers to an org located on the 'NA21' +#' server instance located in Chicago, USA / Washington DC, USA per this +#' Knowledge Article: \url{https://help.salesforce.com/articleView?id=000314281}. +#' } +#' } #' @examples #' \dontrun{ #' # log in using basic authentication (username-password) @@ -66,15 +92,14 @@ #' password = "test_password", #' security_token = "test_token") #' -#' # log in using OAuth 2.0 -#' # Via brower or refresh of .httr-oauth-salesforcer +#' # log in using OAuth 2.0 (via browser or cached .httr-oauth-salesforcer) #' sf_auth() #' #' # log in to a Sandbox environment #' # Via brower or refresh of .httr-oauth-salesforcer #' sf_auth(login_url = "https://test.salesforce.com") #' -#' # Save token and log in using it +#' # Save token to disk and log in using it #' saveRDS(salesforcer_state()$token, "token.rds") #' sf_auth(token = "token.rds") #' } @@ -218,7 +243,12 @@ sf_auth <- function(username = NULL, #' Check that token appears to be legitimate #' +#' @param x an object that is supposed to be an object of class \code{Token2.0} +#' (an S3 class provided by \code{httr}). If so, the result will return \code{TRUE}. +#' @template verbose +#' @return \code{logical} #' @keywords internal +#' @export is_legit_token <- function(x, verbose = FALSE) { if (!inherits(x, "Token2.0")) { @@ -311,7 +341,7 @@ sf_auth_refresh <- function(verbose = FALSE) { #' Check if a session_id is available in \code{\link{salesforcer}}'s internal #' \code{.state} environment. #' -#' @return logical +#' @return \code{logical} #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -335,11 +365,11 @@ session_id_available <- function(verbose = TRUE) { #' Check if a token is available in \code{\link{salesforcer}}'s internal #' \code{.state} environment. #' -#' @return logical +#' @return \code{logical} #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export -token_available <- function(verbose = TRUE) { +token_available <- function(verbose = FALSE) { if (is.null(.state$token)) { if (verbose) { if (file.exists(".httr-oauth-salesforcer")) { diff --git a/R/bulk-operation.R b/R/bulk-operation.R index c2ce0558..19665378 100644 --- a/R/bulk-operation.R +++ b/R/bulk-operation.R @@ -1,8 +1,8 @@ #' Create Bulk API Job #' #' This function initializes a Job in the Salesforce Bulk API -#' -#' @importFrom lifecycle deprecate_warn is_present deprecated +#' +#' @importFrom lifecycle deprecated is_present deprecate_warn #' @template operation #' @template object_name #' @template soql @@ -126,8 +126,23 @@ sf_create_job_bulk <- function(operation = c("insert", "delete", "upsert", "upda #' @importFrom xml2 xml_new_document xml_add_child xml_add_sibling #' @importFrom httr content #' @importFrom XML xmlToList +#' @template operation +#' @template object_name +#' @template external_id_fieldname +#' @param content_type \code{character}; a string indicating the format for +#' the API request and response. Must be one of 'CSV', 'ZIP_CSV', 'ZIP_XML', or +#' 'ZIP_JSON'. +#' @param concurrency_mode \code{character}; a string indicating whether the batches +#' should be processed in parallel or serially (sequentially). Serial processing +#' is helpful when multiple records may trigger simultaneous edits to another +#' related record (e.g., updating multiple children all on the same account). +#' @template control +#' @param ... arguments to be used to form the default control argument if it is not supplied directly. +#' @template verbose +#' @return \code{tbl_df}; a data frame containing information about the job created. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal +#' @export sf_create_job_bulk_v1 <- function(operation = c("insert", "delete", "upsert", "update", "hardDelete", "query", "queryall"), object_name, @@ -213,8 +228,23 @@ sf_create_job_bulk_v1 <- function(operation = c("insert", "delete", "upsert", "u #' @importFrom xml2 xml_new_document xml_add_child xml_add_sibling #' @importFrom httr content #' @importFrom jsonlite toJSON prettify +#' @template operation +#' @template object_name +#' @template soql +#' @template external_id_fieldname +#' @param content_type \code{character}; a string indicating the format for +#' the API request and response. Must be 'CSV' because it is the only supported +#' format for the Bulk 2.0 API. +#' @param column_delimiter \code{character}; a string indicating which character +#' should be treated as the delimiter in the CSV file. Must be one of 'COMMA', +#' 'TAB', 'PIPE', 'SEMICOLON', 'CARET', or 'BACKQUOTE'. +#' @template control +#' @param ... arguments to be used to form the default control argument if it is not supplied directly. +#' @template verbose +#' @return \code{tbl_df}; a data frame containing information about the job created. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal +#' @export sf_create_job_bulk_v2 <- function(operation = c("insert", "delete", "upsert", "update", "query", "queryall"), @@ -370,13 +400,13 @@ sf_get_job_bulk <- function(job_id, #' of the URL query string (i.e. after a question mark ("?") so that the result #' only returns information about jobs that meet that specific criteria. For #' more information, read the note below and/or the Salesforce documentation -#' \href{https://developer.salesforce.com/docs/atlas.en-us.api_bulk_v2.meta/api_bulk_v2/get_all_jobs.htm}{here}. +#' \href{https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/get_all_jobs.htm}{here}. #' @param next_records_url character (leave as NULL); a string used internally #' by the function to paginate through to more records until complete #' @template api_type #' @template verbose #' @return A \code{tbl_df} of parameters defining the details of all bulk jobs -#' @references \url{https://developer.salesforce.com/docs/atlas.en-us.api_bulk_v2.meta/api_bulk_v2/get_all_jobs.htm} +#' @references \url{https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/get_all_jobs.htm} #' @note parameterized_search_list elements that can be set to filter the results: #' \itemize{ #' \item{isPkChunkingEnabled}{A logical either TRUE or FALSE. TRUE only returns @@ -448,13 +478,13 @@ sf_get_all_jobs_bulk <- function(parameterized_search_list = #' of the URL query string (i.e. after a question mark ("?") so that the result #' only returns information about jobs that meet that specific criteria. For #' more information, read the note below and/or the Salesforce documentation -#' \href{https://developer.salesforce.com/docs/atlas.en-us.api_bulk_v2.meta/api_bulk_v2/query_get_all_jobs.htm}{here}. +#' \href{https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/query_get_all_jobs.htm}{here}. #' @param next_records_url character (leave as NULL); a string used internally #' by the function to paginate through to more records until complete #' @template api_type #' @template verbose #' @return A \code{tbl_df} of parameters defining the details of all bulk jobs -#' @references \url{https://developer.salesforce.com/docs/atlas.en-us.api_bulk_v2.meta/api_bulk_v2/get_all_jobs.htm} +#' @references \url{https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/get_all_jobs.htm} #' @note parameterized_search_list elements that can be set to filter the results: #' \itemize{ #' \item{isPkChunkingEnabled}{A logical either TRUE or FALSE. TRUE only returns @@ -536,6 +566,8 @@ sf_get_all_query_jobs_bulk <- function(parameterized_search_list = #' how the bulk job should be ended #' @template api_type #' @template verbose +#' @return \code{logical}; returns \code{TRUE} if the job was able to be ended; +#' otherwise, an error message is printed #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -547,7 +579,7 @@ sf_end_job_bulk <- function(job_id, end_type <- match.arg(end_type) api_type <- match.arg(api_type) if(api_type == "Bulk 2.0" & end_type == "Closed"){ - end_typ <- "UploadComplete" + end_type <- "UploadComplete" } request_body <- toJSON(list(state=end_type), auto_unbox = TRUE) bulk_end_job_url <- make_bulk_end_job_generic_url(job_id, api_type) @@ -925,7 +957,7 @@ sf_create_batches_bulk_v2 <- function(job_id, # encoded content. When job data is uploaded, it is converted to base64. This # conversion can increase the data size by approximately 50%. To account for # the base64 conversion increase, upload data that does not exceed 100 MB. - # https://developer.salesforce.com/docs/atlas.en-us.api_bulk_v2.meta/api_bulk_v2/upload_job_data.htm + # https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/upload_job_data.htm if(is.null(batch_size)){ data_size <- object.size(input_data) numb_batches <- ceiling((as.numeric(data_size)/(1024^2))/100) # 100MB / (size converted to MB) @@ -1088,7 +1120,7 @@ sf_batch_status_bulk <- function(job_id, batch_id, #' This function returns detailed (row-by-row) information on an existing batch #' which has already been submitted to Bulk API Job #' -#' @importFrom readr read_csv +#' @importFrom readr read_csv type_convert cols col_character #' @importFrom httr content #' @importFrom XML xmlToList #' @importFrom dplyr as_tibble @@ -1127,7 +1159,9 @@ sf_batch_details_bulk <- function(job_id, batch_id, content_type <- httr_response$headers$`content-type` if(content_type == 'text/csv' | content_type == 'zip/csv'){ response_text <- content(httr_response, as="text", encoding="UTF-8") - res <- read_csv(response_text) + # required to guess column types by all values in the column, not just first N + res <- read_csv(response_text, col_types = cols(.default = col_character())) + res <- type_convert(res, col_types = cols()) } else if(content_type == 'zip/xml'){ response_parsed <- content(httr_response, as="parsed", type="text/xml", encoding="UTF-8") res <- response_parsed %>% @@ -1160,11 +1194,8 @@ sf_batch_details_bulk <- function(job_id, batch_id, #' #' @template job_id #' @template api_type -#' @param record_types \code{character}; one or more types of records to retrieve from -#' the results of running the specified job -#' @param combine_record_types \code{logical}; indicating for Bulk 2.0 jobs whether the -#' successfulResults, failedResults, and unprocessedRecords should be stacked -#' together by binding the rows. +#' @template record_types +#' @template combine_record_types #' @template verbose #' @return A \code{tbl_df} or \code{list} of \code{tbl_df}, formatted by Salesforce, #' with information containing the success or failure or certain rows in a submitted job @@ -1187,7 +1218,7 @@ sf_get_job_records_bulk <- function(job_id, record_types = c("successfulResults", "failedResults", "unprocessedRecords"), - combine_record_types = TRUE, + combine_record_types = TRUE, verbose = FALSE){ api_type <- match.arg(api_type) if(api_type == "Bulk 1.0"){ @@ -1217,7 +1248,7 @@ sf_get_job_records_bulk_v1 <- function(job_id, verbose = FALSE){ return(resultset) } -#' @importFrom readr read_csv +#' @importFrom readr read_csv type_convert cols col_character #' @importFrom httr content sf_get_job_records_bulk_v2 <- function(job_id, record_types = c("successfulResults", @@ -1240,7 +1271,9 @@ sf_get_job_records_bulk_v2 <- function(job_id, response_text <- content(httr_response, as="text", encoding="UTF-8") content_type <- httr_response$headers$`content-type` if(grepl('text/csv', content_type)) { - res <- read_csv(response_text) + # required to guess column types by all values in the column, not just first N + res <- read_csv(response_text, col_types = cols(.default = col_character())) + res <- type_convert(res, col_types = cols()) } else { message(sprintf("Unhandled content-type: %s", content_type)) res <- content(httr_response, as="parsed", encoding="UTF-8") @@ -1256,11 +1289,13 @@ sf_get_job_records_bulk_v2 <- function(job_id, # First, determine the column datatypes from the data.frame with most rows base_class_df <- records[[head(which.max(sapply(records, nrow)), 1)]] # Second, convert the datatypes for any data.frames with zero rows + # From StackOverflow post here: https://stackoverflow.com/a/47800157/5258043 + # distributed under the CC BY-SA 3.0 license terms. for (i in 1:length(records)){ if(nrow(records[[i]]) == 0){ common <- names(records[[i]])[names(records[[i]]) %in% names(base_class_df)] records[[i]][common] <- lapply(common, function(x) { - match.fun(paste0("as.", class(base_class_df[[x]])))(records[[i]][[x]]) + match.fun(paste0("as.", class(base_class_df[[x]])[1]))(records[[i]][[x]]) }) } } @@ -1275,7 +1310,7 @@ sf_get_job_records_bulk_v2 <- function(job_id, #' Run Bulk Operation #' #' @description -#' `r lifecycle::badge("maturing")` +#' `r lifecycle::badge("stable")` #' #' This function is a convenience wrapper for submitting bulk API jobs #' @@ -1292,6 +1327,8 @@ sf_get_job_records_bulk_v2 <- function(job_id, #' @template max_attempts #' @param wait_for_results \code{logical}; indicating whether to wait for the operation to complete #' so that the batch results of individual records can be obtained +#' @template record_types +#' @template combine_record_types #' @template control #' @param ... other arguments passed on to \code{\link{sf_control}} or \code{\link{sf_create_job_bulk}} #' to specify the \code{content_type}, \code{concurrency_mode}, and/or \code{column_delimiter}. @@ -1322,6 +1359,10 @@ sf_run_bulk_operation <- function(input_data, interval_seconds = 3, max_attempts = 200, wait_for_results = TRUE, + record_types = c("successfulResults", + "failedResults", + "unprocessedRecords"), + combine_record_types = TRUE, control = list(...), ..., verbose = FALSE){ @@ -1392,7 +1433,13 @@ sf_run_bulk_operation <- function(input_data, message("Function's Time Limit Exceeded. Aborting Job Now") res <- sf_abort_job_bulk(job_info$id, api_type = api_type, verbose = verbose) } else { - res <- sf_get_job_records_bulk(job_info$id, api_type = api_type, verbose = verbose) + res <- sf_get_job_records_bulk( + job_info$id, + api_type = api_type, + record_types = record_types, + combine_record_types = combine_record_types, + verbose = verbose + ) # For Bulk 2.0 jobs -> INVALIDJOBSTATE: Closing already Completed Job not allowed if(api_type == "Bulk 1.0"){ close_job_info <- sf_close_job_bulk(job_info$id, api_type = api_type, verbose = verbose) diff --git a/R/bulk-query.R b/R/bulk-query.R index e5945782..e58a070f 100644 --- a/R/bulk-query.R +++ b/R/bulk-query.R @@ -75,7 +75,7 @@ sf_submit_query_bulk <- function(job_id, #' @template api_type #' @template verbose #' @return \code{tbl_df}, formatted by Salesforce, containing query results -#' @references \href{https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/asynch_api_bulk_query_intro.htm}{Bulk 1.0 documentation} and \href{https://developer.salesforce.com/docs/atlas.en-us.api_bulk_v2.meta/api_bulk_v2/queries.htm}{Bulk 2.0 documentation} +#' @references \href{https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/asynch_api_bulk_query_intro.htm}{Bulk 1.0 documentation} and \href{https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/queries.htm}{Bulk 2.0 documentation} #' @examples #' \dontrun{ #' my_query <- "SELECT Id, Name FROM Account LIMIT 1000" @@ -93,15 +93,25 @@ sf_query_result_bulk <- function(job_id, batch_id = NULL, result_id = NULL, guess_types = TRUE, - bind_using_character_cols = FALSE, + bind_using_character_cols = deprecated(), batch_size = 50000, api_type = c("Bulk 1.0", "Bulk 2.0"), verbose = FALSE){ api_type <- match.arg(api_type) + + if(is_present(bind_using_character_cols)) { + deprecate_warn("1.0.0", + "salesforcer::sf_query_result_bulk(bind_using_character_cols)", + details = paste0("The `bind_using_character_cols` functionality ", + "will always be `TRUE` going forward. Per the ", + "{readr} package, we have to read as character ", + "and then invoke `type_convert()` in order to ", + "use all values in a column to guess its type.")) + } + if(api_type == "Bulk 2.0"){ resultset <- sf_query_result_bulk_v2(job_id = job_id, guess_types = guess_types, - bind_using_character_cols = bind_using_character_cols, batch_size = batch_size, locator = NULL, api_type = api_type, @@ -111,7 +121,6 @@ sf_query_result_bulk <- function(job_id, batch_id = batch_id, result_id = result_id, guess_types = guess_types, - bind_using_character_cols = bind_using_character_cols, api_type = api_type, verbose = verbose) } @@ -124,7 +133,8 @@ sf_query_result_bulk <- function(job_id, #' This function returns the row-level recordset of a Bulk 1.0 query #' which has already been submitted to Bulk API Job and has Completed state #' -#' @importFrom readr col_guess col_character +#' @importFrom lifecycle deprecated is_present deprecate_warn +#' @importFrom readr read_csv cols col_character #' @importFrom httr content #' @importFrom XML xmlToList #' @importFrom dplyr is.tbl as_tibble tibble select any_of matches everything @@ -156,10 +166,21 @@ sf_query_result_bulk_v1 <- function(job_id, batch_id = NULL, result_id = NULL, guess_types = TRUE, - bind_using_character_cols = FALSE, + bind_using_character_cols = deprecated(), api_type = c("Bulk 1.0"), verbose = FALSE){ api_type <- match.arg(api_type) + + if(is_present(bind_using_character_cols)) { + deprecate_warn("1.0.0", + "salesforcer::sf_query_result_bulk_v1(bind_using_character_cols)", + details = paste0("The `bind_using_character_cols` functionality ", + "will always be `TRUE` going forward. Per the ", + "{readr} package, we have to read as character ", + "and then invoke `type_convert()` in order to ", + "use all values in a column to guess its type.")) + } + bulk_query_result_url <- make_bulk_query_result_url(job_id, batch_id, result_id, api_type) httr_response <- rGET(url = bulk_query_result_url) if(verbose){ @@ -168,19 +189,19 @@ sf_query_result_bulk_v1 <- function(job_id, httr_response$request$headers) } catch_errors(httr_response) - response_text <- content(httr_response, type="text/plain", encoding="UTF-8") content_type <- httr_response$headers$`content-type` if (grepl('xml', content_type)) { + response_text <- content(httr_response, type="text/plain", encoding="UTF-8") resultset <- as_tibble(xmlToList(response_text)) } else if(grepl('text/csv', content_type)) { + response_text <- content(httr_response, type="text", encoding="UTF-8") if(response_text == "Records not found for this query"){ resultset <- tibble() } else { - cols_default <- if(bind_using_character_cols | - !guess_types) col_character() else col_guess() - resultset <- content(httr_response, as="parsed", encoding="UTF-8", - col_types = cols(.default=cols_default)) + # required to load as character in order to guess column types by all values + # in the column, not just first N + resultset <- read_csv(response_text, col_types = cols(.default = col_character())) } } else { message(sprintf("Unhandled content-type: %s", content_type)) @@ -196,14 +217,14 @@ sf_query_result_bulk_v1 <- function(job_id, return(resultset) } - #' Retrieve the results of a Bulk 2.0 query #' #' This function returns the row-level recordset of a Bulk 2.0 query #' which has already been submitted as a Bulk 2.0 API job and has a JobComplete #' state. #' -#' @importFrom readr col_guess col_character type_convert +#' @importFrom lifecycle deprecated is_present deprecate_warn +#' @importFrom readr read_csv cols col_character #' @importFrom httr content parse_url build_url #' @importFrom dplyr is.tbl select any_of contains #' @template job_id @@ -217,7 +238,7 @@ sf_query_result_bulk_v1 <- function(job_id, #' @template api_type #' @template verbose #' @return \code{tbl_df}, formatted by Salesforce, containing query results -#' @references \href{https://developer.salesforce.com/docs/atlas.en-us.api_bulk_v2.meta/api_bulk_v2/queries.htm}{Bulk 2.0 documentation} +#' @references \href{https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/queries.htm}{Bulk 2.0 documentation} #' @examples #' \dontrun{ #' my_query <- "SELECT Id, Name FROM Account LIMIT 1000" @@ -233,13 +254,23 @@ sf_query_result_bulk_v1 <- function(job_id, #' @export sf_query_result_bulk_v2 <- function(job_id, guess_types = TRUE, - bind_using_character_cols = FALSE, + bind_using_character_cols = deprecated(), batch_size = 50000, locator = NULL, api_type = c("Bulk 2.0"), verbose = FALSE){ api_type <- match.arg(api_type) + + if(is_present(bind_using_character_cols)) { + deprecate_warn("1.0.0", + "salesforcer::sf_query_result_bulk_v2(bind_using_character_cols)", + details = paste0("The `bind_using_character_cols` functionality ", + "will always be `TRUE` going forward. Per the ", + "{readr} package, we have to read as character ", + "and then invoke `type_convert()` in order to ", + "use all values in a column to guess its type.")) + } # construct the url for requesting the records bulk_query_result_url <- make_bulk_query_result_url(job_id, api_type=api_type) @@ -255,13 +286,12 @@ sf_query_result_bulk_v2 <- function(job_id, httr_response$request$headers) } catch_errors(httr_response) - content_type <- httr_response$headers$`content-type` if(grepl('text/csv', content_type)) { - cols_default <- if(bind_using_character_cols | - !guess_types) col_character() else col_guess() - resultset <- content(httr_response, as="parsed", encoding="UTF-8", - col_types = cols(.default=cols_default)) + response_text <- content(httr_response, as="text", encoding="UTF-8") + # required to load as character in order to guess column types by all values + # in the column, not just first N + resultset <- read_csv(response_text, col_types = cols(.default = col_character())) } else { message(sprintf("Unexpected content-type: %s", content_type)) resultset <- content(httr_response, as="parsed", encoding="UTF-8") @@ -272,7 +302,6 @@ sf_query_result_bulk_v2 <- function(job_id, if(!is.null(locator) && locator != "null"){ next_records <- sf_query_result_bulk_v2(job_id = job_id, guess_types = guess_types, - bind_using_character_cols = bind_using_character_cols, batch_size = batch_size, locator = locator, api_type = api_type, @@ -294,6 +323,7 @@ sf_query_result_bulk_v2 <- function(job_id, #' This function is a convenience wrapper for submitting and retrieving #' query API jobs from the Bulk 1.0 API. #' +#' @importFrom lifecycle deprecated is_present deprecate_warn #' @importFrom dplyr filter across any_of bind_rows is.tbl #' @template soql #' @template object_name @@ -324,7 +354,7 @@ sf_query_bulk_v1 <- function(soql, object_name = NULL, queryall = FALSE, guess_types = TRUE, - bind_using_character_cols = FALSE, + bind_using_character_cols = deprecated(), interval_seconds = 3, max_attempts = 200, control = list(...), ..., @@ -350,6 +380,16 @@ sf_query_bulk_v1 <- function(soql, this_operation <- if(queryall) "queryall" else "query" control_args$operation <- this_operation + if(is_present(bind_using_character_cols)) { + deprecate_warn("1.0.0", + "salesforcer::sf_query_bulk_v1(bind_using_character_cols)", + details = paste0("The `bind_using_character_cols` functionality ", + "will always be `TRUE` going forward. Per the ", + "{readr} package, we have to read as character ", + "and then invoke `type_convert()` in order to ", + "use all values in a column to guess its type.")) + } + job_info <- sf_create_job_bulk(operation = this_operation, object_name = object_name, api_type = api_type, @@ -411,7 +451,6 @@ sf_query_bulk_v1 <- function(soql, batch_id = batch_query_info$id[i], result_id = batch_query_details$result, guess_types = guess_types, - bind_using_character_cols = bind_using_character_cols, api_type = api_type, verbose = verbose) } @@ -436,6 +475,7 @@ sf_query_bulk_v1 <- function(soql, #' This function is a convenience wrapper for submitting and retrieving #' query API jobs from the Bulk 2.0 API. #' +#' @importFrom lifecycle deprecated is_present deprecate_warn #' @template soql #' @template object_name #' @template queryall @@ -449,7 +489,7 @@ sf_query_bulk_v1 <- function(soql, #' @template api_type #' @template verbose #' @return A \code{tbl_df} of the recordset returned by the query -#' @references \href{https://developer.salesforce.com/docs/atlas.en-us.api_bulk_v2.meta/api_bulk_v2/queries.htm}{Bulk 2.0 documentation} +#' @references \href{https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/queries.htm}{Bulk 2.0 documentation} #' @examples #' \dontrun{ #' # select all Ids from Account object (up to 1000) @@ -465,7 +505,7 @@ sf_query_bulk_v2 <- function(soql, object_name = NULL, queryall = FALSE, guess_types = TRUE, - bind_using_character_cols = FALSE, + bind_using_character_cols = deprecated(), interval_seconds = 3, max_attempts = 200, control = list(...), ..., @@ -494,6 +534,16 @@ sf_query_bulk_v2 <- function(soql, this_operation <- if(queryall) "queryall" else "query" control_args$operation <- this_operation + if(is_present(bind_using_character_cols)) { + deprecate_warn("1.0.0", + "salesforcer::sf_query_bulk_v2(bind_using_character_cols)", + details = paste0("The `bind_using_character_cols` functionality ", + "will always be `TRUE` going forward. Per the ", + "{readr} package, we have to read as character ", + "and then invoke `type_convert()` in order to ", + "use all values in a column to guess its type.")) + } + # save out the query batch size control because for the Bulk 2.0 API # it is not a header argument, it's actually a query parameter and, # thus, needs to be passed in differently @@ -539,7 +589,6 @@ sf_query_bulk_v2 <- function(soql, } else { res <- sf_query_result_bulk(job_id = job_info$id, guess_types = guess_types, - bind_using_character_cols = bind_using_character_cols, batch_size = batch_size, api_type = api_type, verbose = verbose) @@ -550,11 +599,12 @@ sf_query_bulk_v2 <- function(soql, #' Run bulk query #' #' @description -#' `r lifecycle::badge("maturing")` +#' `r lifecycle::badge("stable")` #' #' This function is a convenience wrapper for submitting and retrieving #' query API jobs from the Bulk 1.0 and Bulk 2.0 APIs. #' +#' @importFrom lifecycle deprecated is_present deprecate_warn #' @template soql #' @template object_name #' @template queryall @@ -569,7 +619,7 @@ sf_query_bulk_v2 <- function(soql, #' @template api_type #' @template verbose #' @return A \code{tbl_df} of the recordset returned by the query -#' @references \href{https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/asynch_api_bulk_query_intro.htm}{Bulk 1.0 documentation} and \href{https://developer.salesforce.com/docs/atlas.en-us.api_bulk_v2.meta/api_bulk_v2/queries.htm}{Bulk 2.0 documentation} +#' @references \href{https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/asynch_api_bulk_query_intro.htm}{Bulk 1.0 documentation} and \href{https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/queries.htm}{Bulk 2.0 documentation} #' @examples #' \dontrun{ #' # select all Ids from Account object (up to 1000) @@ -592,7 +642,7 @@ sf_run_bulk_query <- function(soql, object_name = NULL, queryall = FALSE, guess_types = TRUE, - bind_using_character_cols = FALSE, + bind_using_character_cols = deprecated(), interval_seconds = 3, max_attempts = 200, control = list(...), ..., @@ -607,14 +657,23 @@ sf_run_bulk_query <- function(soql, # It should be a relatively small performance hit given its a bulk operation. control_args <- return_matching_controls(control) control_args$api_type <- api_type - control_args$operation <- if(queryall) "queryall" else "query" - + control_args$operation <- if(queryall) "queryall" else "query" + + if(is_present(bind_using_character_cols)) { + deprecate_warn("1.0.0", + "salesforcer::sf_run_bulk_query(bind_using_character_cols)", + details = paste0("The `bind_using_character_cols` functionality ", + "will always be `TRUE` going forward. Per the ", + "{readr} package, we have to read as character ", + "and then invoke `type_convert()` in order to ", + "use all values in a column to guess its type.")) + } + if(api_type == "Bulk 2.0"){ resultset <- sf_query_bulk_v2(soql = soql, object_name = object_name, queryall = queryall, guess_types = guess_types, - bind_using_character_cols = bind_using_character_cols, interval_seconds = interval_seconds, max_attempts = max_attempts, control = control_args, ..., diff --git a/R/compatibility.R b/R/compatibility.R index f5e6a865..2e466e02 100644 --- a/R/compatibility.R +++ b/R/compatibility.R @@ -21,14 +21,14 @@ # limitations under the License. #' The \code{salesforcer} backwards compatible version of -#' \code{\link[RForcecom]{rforcecom.login}} +#' \code{RForcecom::rforcecom.login} #' #' @description #' `r lifecycle::badge("soft-deprecated")` #' #' @param username Your username for login to the Salesforce.com. In many cases, username is your E-mail address. #' @param password Your password for login to the Salesforce.com. Note: DO NOT FORGET your Security Token. (Ex.) If your password is "Pass1234" and your security token is "XYZXYZXYZXYZ", you should set "Pass1234XYZXYZXYZXYZ". -#' @param loginURL (optional) Login URL. If your environment is sandbox specify (ex:) "https://test.salesforce.com/". +#' @param loginURL (optional) Login URL. If your environment is sandbox specify (ex:) "https://test.salesforce.com". #' @param apiVersion (optional) Version of the REST API and SOAP API that you want to use. (ex:) "35.0" Supported versions from v20.0 and up. #' @return #' \item{sessionID}{Session ID.} @@ -41,11 +41,11 @@ rforcecom.login <- function(username, password, loginURL="https://login.salesfor if(!is.null(loginURL)){ options(salesforcer.login_url = loginURL) - #message("Ignoring loginURL. If needed, set in options like so: options(salesforcer.login_url = \"https://login.salesforce.com\")") + # message("Ignoring loginURL. If needed, set in options like so: options(salesforcer.login_url = \"https://login.salesforce.com\")") } if(!is.null(apiVersion)){ options(salesforcer.api_version = apiVersion) - #message("Ignoring apiVersion. If needed, set in options like so: options(salesforcer.api_version = \"42.0\")") + # message("Ignoring apiVersion. If needed, set in options like so: options(salesforcer.api_version = \"42.0\")") } sf_auth(username=username, @@ -61,7 +61,7 @@ rforcecom.login <- function(username, password, loginURL="https://login.salesfor } #' The \code{salesforcer} backwards compatible version of -#' \code{\link[RForcecom]{rforcecom.getServerTimestamp}} +#' \code{RForcecom::rforcecom.getServerTimestamp} #' #' @description #' `r lifecycle::badge("soft-deprecated")` @@ -79,7 +79,7 @@ rforcecom.getServerTimestamp <- function(session){ } #' The \code{salesforcer} backwards compatible version of -#' \code{\link[RForcecom]{rforcecom.getObjectDescription}} +#' \code{RForcecom::rforcecom.getObjectDescription} #' #' @description #' `r lifecycle::badge("soft-deprecated")` @@ -104,7 +104,7 @@ rforcecom.getObjectDescription <- function(session, objectName){ } #' The \code{salesforcer} backwards compatible version of -#' \code{\link[RForcecom]{rforcecom.create}} +#' \code{RForcecom::rforcecom.create} #' #' @description #' `r lifecycle::badge("soft-deprecated")` @@ -130,7 +130,7 @@ rforcecom.create <- function(session, objectName, fields){ } #' The \code{salesforcer} backwards compatible version of -#' \code{\link[RForcecom]{rforcecom.retrieve}} +#' \code{RForcecom::rforcecom.retrieve} #' #' @description #' `r lifecycle::badge("soft-deprecated")` @@ -195,7 +195,7 @@ rforcecom.retrieve <- function(session, objectName, } #' The \code{salesforcer} backwards compatible version of -#' \code{\link[RForcecom]{rforcecom.update}} +#' \code{RForcecom::rforcecom.update} #' #' @description #' `r lifecycle::badge("soft-deprecated")` @@ -218,7 +218,7 @@ rforcecom.update <- function(session, objectName, id, fields){ } #' The \code{salesforcer} backwards compatible version of -#' \code{\link[RForcecom]{rforcecom.delete}} +#' \code{RForcecom::rforcecom.delete} #' #' @description #' `r lifecycle::badge("soft-deprecated")` @@ -238,7 +238,7 @@ rforcecom.delete <- function(session, objectName, id){ } #' The \code{salesforcer} backwards compatible version of -#' \code{\link[RForcecom]{rforcecom.upsert}} +#' \code{RForcecom::rforcecom.upsert} #' #' @description #' `r lifecycle::badge("soft-deprecated")` @@ -266,7 +266,7 @@ rforcecom.upsert <- function(session, objectName, } #' The \code{salesforcer} backwards compatible version of -#' \code{\link[RForcecom]{rforcecom.search}} +#' \code{RForcecom::rforcecom.search} #' #' @description #' `r lifecycle::badge("soft-deprecated")` @@ -284,7 +284,7 @@ rforcecom.search <- function(session, queryString){ } #' The \code{salesforcer} backwards compatible version of -#' \code{\link[RForcecom]{rforcecom.query}} +#' \code{RForcecom::rforcecom.query} #' #' @description #' `r lifecycle::badge("soft-deprecated")` @@ -303,7 +303,7 @@ rforcecom.query <- function(session, soqlQuery, queryAll=FALSE){ } #' The \code{salesforcer} backwards compatible version of -#' \code{\link[RForcecom]{rforcecom.bulkQuery}} +#' \code{RForcecom::rforcecom.bulkQuery} #' #' @description #' `r lifecycle::badge("soft-deprecated")` diff --git a/R/create.R b/R/create.R index ae74bad3..ad740c1f 100644 --- a/R/create.R +++ b/R/create.R @@ -1,7 +1,7 @@ #' Create Records #' #' @description -#' `r lifecycle::badge("maturing")` +#' `r lifecycle::badge("stable")` #' #' Adds one or more new records to your organization’s data. #' diff --git a/R/delete.R b/R/delete.R index b994562e..6cb5de5e 100644 --- a/R/delete.R +++ b/R/delete.R @@ -1,7 +1,7 @@ #' Delete Records #' #' @description -#' `r lifecycle::badge("maturing")` +#' `r lifecycle::badge("stable")` #' #' Deletes one or more records from your organization’s data. #' diff --git a/R/describe.R b/R/describe.R index b828a0aa..e601d778 100644 --- a/R/describe.R +++ b/R/describe.R @@ -1,7 +1,7 @@ #' SObject Basic Information #' #' @description -#' `r lifecycle::badge("maturing")` +#' `r lifecycle::badge("stable")` #' #' Describes the individual metadata for the specified object. #' diff --git a/R/endpoints-analytics-dashboard.R b/R/endpoints-analytics-dashboard.R index e7b1d41d..3f49ce79 100644 --- a/R/endpoints-analytics-dashboard.R +++ b/R/endpoints-analytics-dashboard.R @@ -1,5 +1,10 @@ #' Dashboard filter operators list URL generator #' +#' @param for_dashboards \code{logical}; an indicator of whether the filter is +#' in reference to dashboards or not. +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send Reports and Dashboards API calls to. This URL is specific to your instance +#' and the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -14,6 +19,9 @@ make_dashboard_filter_operators_list_url <- function(for_dashboards=FALSE){ #' Dashboard list URL generator #' +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send Reports and Dashboards API calls to. This URL is specific to your instance +#' and the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -27,6 +35,10 @@ make_dashboards_list_url <- function(){ #' Dashboard status URL generator #' +#' @template dashboard_id +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send Reports and Dashboards API calls to. This URL is specific to your instance +#' and the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -41,6 +53,10 @@ make_dashboard_status_url <- function(dashboard_id){ #' Dashboard describe URL generator #' +#' @template dashboard_id +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send Reports and Dashboards API calls to. This URL is specific to your instance +#' and the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -55,6 +71,10 @@ make_dashboard_describe_url <- function(dashboard_id){ #' Dashboard filter options analysis URL generator #' +#' @template dashboard_id +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send Reports and Dashboards API calls to. This URL is specific to your instance +#' and the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -69,6 +89,10 @@ make_dashboard_filter_options_analysis_url <- function(dashboard_id){ #' Dashboard URL generator #' +#' @template dashboard_id +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send Reports and Dashboards API calls to. This URL is specific to your instance +#' and the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -83,6 +107,10 @@ make_dashboard_url <- function(dashboard_id){ #' Dashboard Copy URL generator #' +#' @template dashboard_id +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send Reports and Dashboards API calls to. This URL is specific to your instance +#' and the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export diff --git a/R/endpoints-analytics-notification.R b/R/endpoints-analytics-notification.R index 5f2aefae..ecbed2ac 100644 --- a/R/endpoints-analytics-notification.R +++ b/R/endpoints-analytics-notification.R @@ -1,5 +1,8 @@ #' Analytics Notification list URL generator #' +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send Analytics notification calls to. This URL is specific to your instance +#' and the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -13,6 +16,9 @@ make_analytics_notifications_list_url <- function(){ #' Analytics Notification limits URL generator #' +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send Analytics notification calls to. This URL is specific to your instance +#' and the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -26,6 +32,9 @@ make_analytics_notifications_limits_url <- function(){ #' Analytics Notification operations URL generator #' +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send Analytics notification calls to. This URL is specific to your instance +#' and the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export diff --git a/R/endpoints-analytics-report-folder.R b/R/endpoints-analytics-report-folder.R index 2efae362..c76509f6 100644 --- a/R/endpoints-analytics-report-folder.R +++ b/R/endpoints-analytics-report-folder.R @@ -1,5 +1,8 @@ #' Analytics Folder collections URL generator #' +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send Analytics folder calls to. This URL is specific to your instance +#' and the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -13,6 +16,10 @@ make_analytics_folder_collections_url <- function(){ #' Analytics Folder operations URL generator #' +#' @template report_folder_id +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send Analytics folder calls to. This URL is specific to your instance +#' and the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -27,6 +34,10 @@ make_analytics_folder_operations_url <- function(report_folder_id){ #' Analytics Folder shares URL generator #' +#' @template report_folder_id +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send Analytics folder calls to. This URL is specific to your instance +#' and the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -41,6 +52,11 @@ make_analytics_folder_shares_url <- function(report_folder_id){ #' Analytics Folder share by Id URL generator #' +#' @template report_folder_id +#' @template share_id +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send Analytics folder calls to. This URL is specific to your instance +#' and the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -56,10 +72,16 @@ make_analytics_folder_share_by_id_url <- function(report_folder_id, share_id){ #' Analytics Folder share recipients URL generator #' +#' @template report_folder_id +#' @param share_type \code{character}; the type of data for the recipients, +#' such as user, group, or role. The default is "User". +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send Analytics folder calls to. This URL is specific to your instance +#' and the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export -make_analytics_folder_share_recipients_url <- function(report_folder_id, share_type){ +make_analytics_folder_share_recipients_url <- function(report_folder_id, share_type="User"){ # ensure we are authenticated first so the url can be formed sf_auth_check() sprintf("%s/services/data/v%s/folders/%s/share-recipients?shareType=%s", @@ -71,6 +93,10 @@ make_analytics_folder_share_recipients_url <- function(report_folder_id, share_t #' Analytics Folder child operations URL generator #' +#' @template report_folder_id +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send Analytics folder calls to. This URL is specific to your instance +#' and the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export diff --git a/R/endpoints-analytics-report.R b/R/endpoints-analytics-report.R index 0f9668ca..fb43de61 100644 --- a/R/endpoints-analytics-report.R +++ b/R/endpoints-analytics-report.R @@ -1,10 +1,13 @@ #' Report Type List URL generator #' +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send Reports and Dashboards API calls to. This URL is specific to your instance +#' and the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export make_report_types_list_url <- function(){ - # ensure we are authenticated first so the url can be formed + # ensure we are authenticated first so the URL can be formed sf_auth_check() sprintf("%s/services/data/v%s/analytics/reportTypes", salesforcer_state()$instance_url, @@ -13,6 +16,9 @@ make_report_types_list_url <- function(){ #' Reports List URL generator #' +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send Reports and Dashboards API calls to. This URL is specific to your instance +#' and the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -26,11 +32,15 @@ make_reports_list_url <- function(){ #' Report Type Describe URL generator #' +#' @param type \code{character}; The API name of a report type. +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send Reports and Dashboards API calls to. This URL is specific to your instance +#' and the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export make_report_type_describe_url <- function(type){ - # ensure we are authenticated first so the url can be formed + # ensure we are authenticated first so the URL can be formed sf_auth_check() sprintf("%s/services/data/v%s/analytics/reportTypes/%s", salesforcer_state()$instance_url, @@ -40,6 +50,11 @@ make_report_type_describe_url <- function(type){ #' Report Filter Operator List URL generator #' +#' @param for_dashboards \code{logical}; an indicator of whether the filter is +#' in reference to dashboards or not. +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send Reports and Dashboards API calls to. This URL is specific to your instance +#' and the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -54,6 +69,10 @@ make_report_filter_operators_list_url <- function(for_dashboards=FALSE){ #' Report Fields URL generator #' +#' @template report_id +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send Reports and Dashboards API calls to. This URL is specific to your instance +#' and the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -68,6 +87,10 @@ make_report_fields_url <- function(report_id){ #' Report Describe URL generator #' +#' @template report_id +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send Reports and Dashboards API calls to. This URL is specific to your instance +#' and the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -82,6 +105,12 @@ make_report_describe_url <- function(report_id){ #' Report Execute URL generator #' +#' @template report_id +#' @template async +#' @template include_details +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send Reports and Dashboards API calls to. This URL is specific to your instance +#' and the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -104,6 +133,10 @@ make_report_execute_url <- function(report_id, async=TRUE, include_details=FALSE #' Report Instances List URL generator #' +#' @template report_id +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send Reports and Dashboards API calls to. This URL is specific to your instance +#' and the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -118,6 +151,11 @@ make_report_instances_list_url <- function(report_id){ #' Report Instance URL generator #' +#' @template report_id +#' @template report_instance_id +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send Reports and Dashboards API calls to. This URL is specific to your instance +#' and the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -133,6 +171,9 @@ make_report_instance_url <- function(report_id, report_instance_id){ #' Report Query URL generator #' +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send Reports and Dashboards API calls to. This URL is specific to your instance +#' and the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -146,6 +187,9 @@ make_report_query_url <- function(){ #' Report Create URL generator #' +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send Reports and Dashboards API calls to. This URL is specific to your instance +#' and the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -159,6 +203,10 @@ make_report_create_url <- function(){ #' Report URL generator #' +#' @template report_id +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send Reports and Dashboards API calls to. This URL is specific to your instance +#' and the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -173,6 +221,10 @@ make_report_url <- function(report_id){ #' Report Copy URL generator #' +#' @template report_id +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send Reports and Dashboards API calls to. This URL is specific to your instance +#' and the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export diff --git a/R/endpoints-bulk.R b/R/endpoints-bulk.R index de51af2a..4951d54e 100644 --- a/R/endpoints-bulk.R +++ b/R/endpoints-bulk.R @@ -1,5 +1,12 @@ #' Bulk Create Job URL Generator #' +#' @param api_type \code{character}; a string indicating which Bulk API to execute +#' the call against. +#' @param query_operation \code{logical}; an indicator of whether the call is +#' for a query or another operation, such as, CRUD operations. +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send subsequent Bulk API calls to. This URL is specific to your instance and +#' the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -27,6 +34,14 @@ make_bulk_create_job_url <- function(api_type=c("Bulk 1.0", "Bulk 2.0"), #' Bulk Get Job Generic URL Generator #' +#' @template job_id +#' @param api_type \code{character}; a string indicating which Bulk API to execute +#' the call against. +#' @param query_operation \code{logical}; an indicator of whether the call is +#' for a query or another operation, such as, CRUD operations. +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send subsequent Bulk API calls to. This URL is specific to your instance and +#' the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -58,6 +73,14 @@ make_bulk_get_job_url <- function(job_id, #' Bulk Get All Jobs Generic URL Generator #' +#' @param parameterized_search_list \code{list}; a list of search options to locate +#' Bulk API jobs. +#' @param next_records_url \code{character}; a string returned by a Salesforce +#' query from where to find subsequent records returned by a paginated query. +#' @param api_type \code{character}; a string indicating which Bulk API to execute +#' the call against. +#' @return \code{character}; a complete URL (as a string) to send a request +#' to in order to retrieve queried jobs. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -92,14 +115,22 @@ make_bulk_get_all_jobs_url <- function(parameterized_search_list = list(isPkChun #' Bulk Get All Query Jobs Generic URL Generator #' +#' @param parameterized_search_list \code{list}; a list of search options to locate +#' Bulk API query jobs. +#' @param next_records_url \code{character}; a string returned by a Salesforce +#' query from where to find subsequent records returned by a paginated query. +#' @param api_type \code{character}; a string indicating which Bulk API to execute +#' the call against. +#' @return \code{character}; a complete URL (as a string) to send a request +#' to in order to retrieve queried jobs. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export make_bulk_get_all_query_jobs_url <- function(parameterized_search_list = list(isPkChunkingEnabled=NULL, - jobType=NULL, - concurrencyMode=NULL), - next_records_url=NULL, - api_type=c("Bulk 2.0")){ + jobType=NULL, + concurrencyMode=NULL), + next_records_url=NULL, + api_type=c("Bulk 2.0")){ # ensure we are authenticated first so the url can be formed sf_auth_check() api_type <- match.arg(api_type) @@ -127,9 +158,15 @@ make_bulk_get_all_query_jobs_url <- function(parameterized_search_list = list(is #' Validate Query Parameters When Getting List of All Bulk Jobs #' +#' @param parameterized_search_list \code{list}; a list of search options to +#' locate Bulk API jobs. +#' @param type \code{character}; a string indicating which type of jobs to +#' include in the result. +#' @return \code{character}; a complete URL (as a string) to send a request +#' to in order to retrieve queried jobs. +#' @seealso \url{https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/query_get_all_jobs.htm} #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal -#' @seealso \url{https://developer.salesforce.com/docs/atlas.en-us.api_bulk_v2.meta/api_bulk_v2/query_get_all_jobs.htm} #' @export validate_get_all_jobs_params <- function(parameterized_search_list, type="all"){ for (n in names(parameterized_search_list)){ @@ -159,6 +196,12 @@ validate_get_all_jobs_params <- function(parameterized_search_list, type="all"){ #' Bulk End Job Generic URL Generator #' +#' @template job_id +#' @param api_type \code{character}; a string indicating which Bulk API to execute +#' the call against. +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send subsequent Bulk API calls to. This URL is specific to your instance and +#' the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -181,6 +224,12 @@ make_bulk_end_job_generic_url <- function(job_id, api_type=c("Bulk 1.0", "Bulk 2 #' Bulk Delete Job Generic URL Generator #' +#' @template job_id +#' @param api_type \code{character}; a string indicating which Bulk API to execute +#' the call against. +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send subsequent Bulk API calls to. This URL is specific to your instance and +#' the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -201,6 +250,12 @@ make_bulk_delete_job_url <- function(job_id, api_type=c("Bulk 2.0")){ #' Bulk Batches URL Generator #' #' @importFrom xml2 url_escape +#' @template job_id +#' @param api_type \code{character}; a string indicating which Bulk API to execute +#' the call against. +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send subsequent Bulk API calls to. This URL is specific to your instance and +#' the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -223,6 +278,12 @@ make_bulk_batches_url <- function(job_id, api_type=c("Bulk 1.0", "Bulk 2.0")){ #' Bulk Query URL Generator #' +#' @template job_id +#' @param api_type \code{character}; a string indicating which Bulk API to execute +#' the call against. +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send subsequent Bulk API calls to. This URL is specific to your instance and +#' the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -248,6 +309,13 @@ make_bulk_query_url <- function(job_id=NULL, #' Bulk Batch Status URL Generator #' +#' @template job_id +#' @template batch_id +#' @param api_type \code{character}; a string indicating which Bulk API to execute +#' the call against. +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send subsequent Bulk API calls to. This URL is specific to your instance and +#' the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -265,6 +333,13 @@ make_bulk_batch_status_url <- function(job_id, batch_id, api_type=c("Bulk 1.0")) #' Bulk Batch Details URL Generator #' +#' @template job_id +#' @template batch_id +#' @param api_type \code{character}; a string indicating which Bulk API to execute +#' the call against. +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send subsequent Bulk API calls to. This URL is specific to your instance and +#' the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -282,6 +357,15 @@ make_bulk_batch_details_url <- function(job_id, batch_id, api_type=c("Bulk 1.0") #' Bulk Query Result URL Generator #' +#' @template job_id +#' @template batch_id +#' @param result_id \code{character}; the Salesforce Id assigned to a generated +#' result for a bulk query batch. +#' @param api_type \code{character}; a string indicating which Bulk API to execute +#' the call against. +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send subsequent Bulk API calls to. This URL is specific to your instance and +#' the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -316,6 +400,14 @@ make_bulk_query_result_url <- function(job_id, #' Bulk Job Records URL Generator #' +#' @template job_id +#' @param record_type \code{character}; one of 'successfulResults', 'failedResults', +#' or 'unprocessedRecords' indicating the type of records to retrieve. +#' @param api_type \code{character}; a string indicating which Bulk API to execute +#' the call against. +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send subsequent Bulk API calls to. This URL is specific to your instance and +#' the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export diff --git a/R/endpoints-rest.R b/R/endpoints-rest.R index 6c6d90bf..cc67dc7a 100644 --- a/R/endpoints-rest.R +++ b/R/endpoints-rest.R @@ -1,5 +1,8 @@ #' Base REST API URL Generator #' +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send REST API calls to. This URL is specific to your instance and the API +#' version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -14,6 +17,9 @@ make_base_rest_url <- function(){ #' REST API Describe URL Generator #' #' @template object_name +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send REST API calls to regarding a specific object. This URL is also specific +#' to your instance and the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -26,21 +32,11 @@ make_rest_describe_url <- function(object_name){ object_name) } -#' Parameterized Search URL Generator -#' -#' @importFrom xml2 url_escape -#' @importFrom httr build_url parse_url -#' @note This function is meant to be used internally. Only use when debugging. -#' @keywords internal -#' @export -make_parameterized_search_url <- function(search_string, params){ - this_url <- parse_url(paste0(make_base_rest_url(), "parameterizedSearch/")) - this_url$query <- c(list(q=url_escape(search_string)), params) - build_url(this_url) -} - #' Chatter Users URL Generator #' +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send REST API calls regarding chatter users. This URL is specific to your +#' instance and the API version because it relies on the base rest URL. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -50,6 +46,9 @@ make_chatter_users_url <- function(){ #' Composite URL Generator #' +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send composite REST API calls to. This URL is specific to your +#' instance and the API version because it relies on the base rest URL. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -60,6 +59,12 @@ make_composite_url <- function(){ #' Query URL Generator #' #' @importFrom xml2 url_escape +#' @template soql +#' @template queryall +#' @param next_records_url \code{character}; a string returned by a Salesforce +#' query from where to find subsequent records returned by a paginated query. +#' @return \code{character}; a complete URL (as a string) to send a request +#' to in order to retrieve queried records. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -86,6 +91,13 @@ make_query_url <- function(soql, queryall, next_records_url){ #' #' @importFrom xml2 url_escape #' @importFrom httr build_url parse_url +#' @param search_string \code{character}; a valid string for conducting a simple +#' RESTful search using parameters instead of a SOSL clause. +#' @param params \code{list}; a list of other values to populate in the URL +#' query string to further restrict the search +#' @return \code{character}; a complete URL (as a string) that has applied the +#' proper escaping and formatting for the search specified by the function inputs. +#' @references \url{https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_search_parameterized.htm} #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -105,6 +117,10 @@ make_parameterized_search_url <- function(search_string=NULL, params=NULL){ #' Search URL Generator #' #' @importFrom xml2 url_escape +#' @param search_string \code{character}; a valid string for conducting a SOSL +#' search. +#' @return \code{character}; a complete URL (as a string) that has applied the +#' proper escaping and formatting for the search specified by the string. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -115,24 +131,36 @@ make_search_url <- function(search_string){ #' REST Objects URL Generator #' +#' @template object_name +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send REST API calls to regarding a specific object. This URL is also specific +#' to your instance and the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export -make_rest_objects_url <- function(object){ - paste0(make_base_rest_url(), "sobjects/", object, "/") +make_rest_objects_url <- function(object_name){ + paste0(make_base_rest_url(), "sobjects/", object_name, "/") } #' REST Individual Record URL Generator #' +#' @template object_name +#' @template sf_id +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send REST API calls to regarding a specific record in an object. This URL is +#' also specific to your instance and the API version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export -make_rest_record_url <- function(object, sf_id){ - paste0(make_base_rest_url(), "sobjects/", object, "/", sf_id) +make_rest_record_url <- function(object_name, sf_id){ + paste0(make_base_rest_url(), "sobjects/", object_name, "/", sf_id) } #' Composite Batch URL Generator #' +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send composite batch REST API calls to. This URL is specific to your instance +#' and the API version being used because it relies on the base REST URL. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export diff --git a/R/endpoints-soap.R b/R/endpoints-soap.R index fc8a1ffb..a1b157be 100644 --- a/R/endpoints-soap.R +++ b/R/endpoints-soap.R @@ -1,6 +1,15 @@ #' Login URL Generator #' -#' @note This function is meant to be used internally. Only use when debugging. +#' @param login_url \code{character}; the package default login URL is +#' https://login.salesforce.com, but other URLs can be used. For example, if you +#' are logging into a sandbox environment, then the login URL should be set to +#' https://test.salesforce.com. +#' @return \code{character}; a complete URL (as a string) that will be used to +#' login to. This string is specific to your environment (production, sandbox, +#' etc.) and the API version being used. +#' @note This function is meant to be used internally. Only use when debugging. +#' You should set the login URL globally as one of the package options: +#' \code{options(salesforcer.login_url="https://test.salesforce.com")}. #' @keywords internal #' @export make_login_url <- function(login_url){ @@ -11,6 +20,9 @@ make_login_url <- function(login_url){ #' Base SOAP API URL Generator #' +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send SOAP API calls to. This URL is specific to your instance and the API +#' version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -24,6 +36,9 @@ make_base_soap_url <- function(){ #' Base Metadata API URL Generator #' +#' @return \code{character}; a complete URL (as a string) that will be used to +#' send Metadata API calls to. This URL is specific to your instance and the API +#' version being used. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export diff --git a/R/query.R b/R/query.R index 30cedcf2..324e8be6 100644 --- a/R/query.R +++ b/R/query.R @@ -1,7 +1,7 @@ #' Perform SOQL Query #' #' @description -#' `r lifecycle::badge("maturing")` +#' `r lifecycle::badge("stable")` #' #' Executes a query against the specified object and returns data that matches #' the specified criteria. @@ -48,7 +48,7 @@ sf_query <- function(soql, control = list(...), ..., page_size = deprecated(), next_records_url = NULL, - bind_using_character_cols = FALSE, + bind_using_character_cols = deprecated(), object_name_append = FALSE, object_name_as_col = FALSE, verbose = FALSE){ @@ -61,12 +61,23 @@ sf_query <- function(soql, control_args$operation <- if(queryall) "queryall" else "query" if(is_present(page_size)) { - deprecate_warn("0.1.3", "salesforcer::sf_query(page_size = )", - "sf_query(QueryOptions = )", + deprecate_warn("0.1.3", + "salesforcer::sf_query(page_size = )", + "salesforcer::sf_query(QueryOptions = )", details = paste0("You can pass the page/batch size directly ", "as shown above or via the `control` argument.")) control_args$QueryOptions <- list(batchSize = as.integer(page_size)) } + + if(is_present(bind_using_character_cols)) { + deprecate_warn("1.0.0", + "salesforcer::sf_query(bind_using_character_cols)", + details = paste0("The `bind_using_character_cols` functionality ", + "will always be `TRUE` going forward. Per the ", + "{readr} package, we have to read as character ", + "and then invoke `type_convert()` in order to ", + "use all values in a column to guess its type.")) + } if(api_type == "REST"){ resultset <- sf_query_rest(soql = soql, @@ -75,7 +86,6 @@ sf_query <- function(soql, guess_types = guess_types, control = control_args, next_records_url = next_records_url, - bind_using_character_cols = bind_using_character_cols, object_name_append = object_name_append, object_name_as_col = object_name_as_col, verbose = verbose) @@ -86,7 +96,6 @@ sf_query <- function(soql, guess_types = guess_types, control = control_args, next_records_url = next_records_url, - bind_using_character_cols = bind_using_character_cols, object_name_append = object_name_append, object_name_as_col = object_name_as_col, verbose = verbose) @@ -125,6 +134,7 @@ sf_query <- function(soql, return(resultset) } +#' @importFrom lifecycle deprecated is_present deprecate_warn #' @importFrom dplyr tibble mutate_all #' @importFrom httr content #' @importFrom purrr map map_df modify map_lgl pluck @@ -134,7 +144,7 @@ sf_query_rest <- function(soql, guess_types = TRUE, control = list(), next_records_url = NULL, - bind_using_character_cols = FALSE, + bind_using_character_cols = deprecated(), object_name_append = FALSE, object_name_as_col = FALSE, verbose = FALSE){ @@ -152,6 +162,16 @@ sf_query_rest <- function(soql, query_batch_size))) } + if(is_present(bind_using_character_cols)) { + deprecate_warn("1.0.0", + "salesforcer::sf_query_rest(bind_using_character_cols)", + details = paste0("The `bind_using_character_cols` functionality ", + "will always be `TRUE` going forward. Per the ", + "{readr} package, we have to read as character ", + "and then invoke `type_convert()` in order to ", + "use all values in a column to guess its type.")) + } + # GET the url with the q (query) parameter set to the escaped SOQL string httr_response <- rGET(url = query_url, headers = request_headers) if(verbose){ @@ -200,7 +220,6 @@ sf_query_rest <- function(soql, queryall = queryall, guess_types = FALSE, control = control, - bind_using_character_cols = bind_using_character_cols, object_name_append = TRUE, object_name_as_col = object_name_as_col, verbose = verbose) @@ -230,9 +249,9 @@ sf_query_rest <- function(soql, # fails. Casting all as character and switching to guess types allows all # pages to be pulled without breaking and then trying to reconcile why # the types were different between the paginated API calls - if(bind_using_character_cols){ - resultset <- resultset %>% mutate_all(as.character) - } + # RESOLUTION (7/4/2021): We must always pull as character in order for type_convert() + # to use all values in the column to determine the type. + resultset <- resultset %>% mutate_all(as.character) # check whether the query has more results to pull via pagination if(!response_parsed$done){ @@ -241,7 +260,6 @@ sf_query_rest <- function(soql, queryall = queryall, guess_types = FALSE, control = control, - bind_using_character_cols = bind_using_character_cols, object_name_append = object_name_append, object_name_as_col = object_name_as_col, verbose = verbose) @@ -255,6 +273,7 @@ sf_query_rest <- function(soql, return(resultset) } +#' @importFrom lifecycle deprecated is_present deprecate_warn #' @importFrom dplyr tibble mutate_all #' @importFrom httr content #' @importFrom purrr map_df modify_if @@ -265,13 +284,23 @@ sf_query_soap <- function(soql, guess_types = TRUE, control = list(), next_records_url = NULL, - bind_using_character_cols = FALSE, + bind_using_character_cols = deprecated(), object_name_append = FALSE, object_name_as_col = FALSE, verbose = FALSE){ control <- filter_valid_controls(control) + if(is_present(bind_using_character_cols)) { + deprecate_warn("1.0.0", + "salesforcer::sf_query_soap(bind_using_character_cols)", + details = paste0("The `bind_using_character_cols` functionality ", + "will always be `TRUE` going forward. Per the ", + "{readr} package, we have to read as character ", + "and then invoke `type_convert()` in order to ", + "use all values in a column to guess its type.")) + } + if(!is.null(next_records_url)){ soap_action <- "queryMore" records_xpath <- './/soapenv:Body/queryMoreResponse/result/records' @@ -335,7 +364,6 @@ sf_query_soap <- function(soql, queryall = queryall, guess_types = FALSE, control = control, - bind_using_character_cols = bind_using_character_cols, object_name_append = TRUE, object_name_as_col = object_name_as_col, verbose = verbose) @@ -378,9 +406,7 @@ sf_query_soap <- function(soql, # neither will a number, so the `as_list()` function will return all values as # characters, which is why we should have the default value of the `guess_types` # argument to be set to TRUE - if(bind_using_character_cols){ - resultset <- resultset %>% mutate_all(as.character) - } + resultset <- resultset %>% mutate_all(as.character) done_status <- response_parsed %>% xml_ns_strip() %>% @@ -397,7 +423,6 @@ sf_query_soap <- function(soql, queryall = queryall, guess_types = FALSE, control = control, - bind_using_character_cols = bind_using_character_cols, object_name_append = object_name_append, object_name_as_col = object_name_as_col, verbose = verbose) diff --git a/R/read-metadata.R b/R/read-metadata.R index 3616ea4c..bb5e9ed9 100644 --- a/R/read-metadata.R +++ b/R/read-metadata.R @@ -75,7 +75,7 @@ sf_read_metadata <- function(metadata_type, object_names, verbose=FALSE){ #' Describe Object Fields #' #' @description -#' `r lifecycle::badge("maturing")` +#' `r lifecycle::badge("stable")` #' #' This function takes the name of an object in Salesforce and returns a description #' of the fields on that object by returning a tibble with one row per field. diff --git a/R/retrieve-metadata.R b/R/retrieve-metadata.R index bc45a393..3ce17613 100644 --- a/R/retrieve-metadata.R +++ b/R/retrieve-metadata.R @@ -109,7 +109,6 @@ sf_retrieve_metadata <- function(retrieve_request, #' @importFrom xml2 xml_ns_strip xml_find_all as_list read_xml #' @importFrom purrr map_df map_dfc #' @importFrom dplyr as_tibble -#' @references \url{https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_checkretrievestatus.htm} #' @param id \code{character}; string id returned from \link{sf_retrieve_metadata} #' @param include_zip \code{logical}; Set to false to check the status of the retrieval without #' attempting to retrieve the zip file. If omitted, this argument defaults to true. @@ -118,6 +117,7 @@ sf_retrieve_metadata <- function(retrieve_request, #' working directory as package.zip #' @template verbose #' @return A \code{list} of the response +#' @references \url{https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_checkretrievestatus.htm} #' @examples #' \dontrun{ #' retrieve_request <- list(unpackaged=list(types=list(members='*', name='CustomObject'))) diff --git a/R/retrieve.R b/R/retrieve.R index 60ef1149..2f7e019f 100644 --- a/R/retrieve.R +++ b/R/retrieve.R @@ -1,7 +1,7 @@ #' Retrieve Records By Id #' #' @description -#' `r lifecycle::badge("maturing")` +#' `r lifecycle::badge("stable")` #' #' Retrieves one or more new records to your organization’s data. #' diff --git a/R/search.R b/R/search.R index 134ba2bb..7073b4e2 100644 --- a/R/search.R +++ b/R/search.R @@ -1,7 +1,7 @@ #' Perform SOSL Search #' #' @description -#' `r lifecycle::badge("maturing")` +#' `r lifecycle::badge("stable")` #' #' Searches for records in your organization’s data. #' diff --git a/R/update.R b/R/update.R index 8d8c41a1..ea7dde07 100644 --- a/R/update.R +++ b/R/update.R @@ -1,7 +1,7 @@ #' Update Records #' #' @description -#' `r lifecycle::badge("maturing")` +#' `r lifecycle::badge("stable")` #' #' Updates one or more records to your organization’s data. #' diff --git a/R/upsert.R b/R/upsert.R index f1b857b3..0bb44a0a 100644 --- a/R/upsert.R +++ b/R/upsert.R @@ -1,7 +1,7 @@ #' Upsert Records #' #' @description -#' `r lifecycle::badge("maturing")` +#' `r lifecycle::badge("stable")` #' #' Upserts one or more new records to your organization’s data. #' diff --git a/R/utils-control.R b/R/utils-control.R index 43c4f789..6dd7db29 100644 --- a/R/utils-control.R +++ b/R/utils-control.R @@ -203,6 +203,9 @@ sf_control <- function(AllOrNoneHeader=list(allOrNone=FALSE), #' Return the Accepted Control Arguments by API Type #' +#' @template api_type +#' @return \code{character}; a vector of strings indicating which control arguments +#' are accepted by the specified API. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -223,6 +226,9 @@ accepted_controls_by_api <- function(api_type = c("SOAP", "REST", "Bulk 1.0", "B #' Return the Accepted Control Arguments by Operation #' +#' @template operation +#' @return \code{character}; a vector of strings indicating which control arguments +#' are accepted by the specified operation. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -260,6 +266,13 @@ accepted_controls_by_operation <- function(operation = c("create" , "insert", #' Filter Out Control Arguments by API or Operation #' +#' @param supplied \code{list}; a list of input data regarding the control arguments +#' along with the with API and operation information to make a complete assessment +#' of which control arguments are applicable. +#' @template api_type +#' @template operation +#' @return \code{character}; a vector of strings returning only the control arguments +#' that are accepted by the specified API and operation. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -306,6 +319,12 @@ filter_valid_controls <- function(supplied, api_type = NULL, operation = NULL){ #' Of All Args Return Ones Matching Control Arguments #' +#' @param args \code{character}; a vector of strings that represent the function +#' arguments. +#' @return \code{character}; a vector of strings returning only the function arguments +#' that match control arguments so that users can specify them directly in each +#' function and not have to construct a control object every time in order to +#' pass only one or two control arguments. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export diff --git a/R/utils-datatypes.R b/R/utils-datatypes.R index f9f9e7bd..da20bf29 100644 --- a/R/utils-datatypes.R +++ b/R/utils-datatypes.R @@ -1,9 +1,14 @@ #' Format Datetimes for Create and Update operations #' -#' @note This function is meant to be used internally. Only use when debugging. #' @importFrom lubridate as_datetime +#' @param x an object, potentially, representing a datetime that should be converted +#' to the Salesforce standard. +#' @return \code{character}; an object where any values that appear to be a date +#' or date time are reformatted as an ISO8601 string per the requirements of the +#' Salesforce APIs. +#' @seealso \url{https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/datafiles_date_format.htm} +#' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal -#' @seealso \url{https://developer.salesforce.com/docs/atlas.en-us.api_bulk_v2.meta/api_bulk_v2/datafiles_date_format.htm} #' @export sf_format_datetime <- function(x){ format(as_datetime(x), "%Y-%m-%dT%H:%M:%SZ") @@ -11,26 +16,39 @@ sf_format_datetime <- function(x){ #' Format Dates for Create and Update operations #' +#' @param x a value representing a datetime +#' @return \code{character}; a date string with the time set to midnight in the +#' user's system timezone and then formatted in ISO8601 per the requirements of +#' the Salesforce APIs. +#' @seealso \url{https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/datafiles_date_format.htm} #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal -#' @seealso \url{https://developer.salesforce.com/docs/atlas.en-us.api_bulk_v2.meta/api_bulk_v2/datafiles_date_format.htm} #' @export sf_format_date <- function(x){ x <- as_datetime(format(x, "%Y-%m-%d 00:00:00"), tz=Sys.timezone()) sf_format_datetime(x) } -sf_format_time <- function (x, ...) { +#' Format all Date and Datetime values in an object +#' +#' @importFrom dplyr mutate_if +#' @importFrom lubridate is.POSIXct is.POSIXlt is.POSIXt is.Date +#' @param x data which may or may not have values, elements, columns that +#' represent a datetime. If so, each of those are cast to the ISO8601 standard +#' per the requirements of Salesforce APIs. +#' @return the same data object with datetime values formatted. +#' @seealso \url{https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/datafiles_date_format.htm} +#' @note This function is meant to be used internally. Only use when debugging. +#' @keywords internal +#' @rdname sf_format_time +#' @export +sf_format_time <- function (x) { UseMethod("sf_format_time", x) } -#' Format all Date and Datetime columns in a list -#' -#' @note This function is meant to be used internally. Only use when debugging. #' @importFrom dplyr mutate_if #' @importFrom lubridate is.POSIXct is.POSIXlt is.POSIXt is.Date -#' @keywords internal -#' @seealso \url{https://developer.salesforce.com/docs/atlas.en-us.api_bulk_v2.meta/api_bulk_v2/datafiles_date_format.htm} +#' @rdname sf_format_time #' @export sf_format_time.list <- function(x){ lapply(x, FUN=function(xx){ @@ -42,13 +60,9 @@ sf_format_time.list <- function(x){ }) } -#' Format all Date and Datetime columns in a dataset -#' -#' @note This function is meant to be used internally. Only use when debugging. #' @importFrom dplyr mutate_if #' @importFrom lubridate is.POSIXct is.POSIXlt is.POSIXt is.Date -#' @keywords internal -#' @seealso \url{https://developer.salesforce.com/docs/atlas.en-us.api_bulk_v2.meta/api_bulk_v2/datafiles_date_format.htm} +#' @rdname sf_format_time #' @export sf_format_time.data.frame <- function(x){ x %>% @@ -58,48 +72,61 @@ sf_format_time.data.frame <- function(x){ mutate_if(is.Date, sf_format_date) } - +#' @rdname sf_format_time #' @export sf_format_time.Date <- function(x){ sf_format_date(x) } +#' @rdname sf_format_time #' @export sf_format_time.POSIXct <- function(x){ sf_format_datetime(x) } +#' @rdname sf_format_time #' @export sf_format_time.POSIXlt <- function(x){ sf_format_datetime(x) } +#' @rdname sf_format_time #' @export sf_format_time.POSIXt <- function(x){ sf_format_datetime(x) } +#' @rdname sf_format_time #' @export sf_format_time.character <- function(x){ x } +#' @rdname sf_format_time #' @export sf_format_time.numeric <- function(x){ x } +#' @rdname sf_format_time #' @export sf_format_time.logical <- function(x){ x } +#' @rdname sf_format_time #' @export sf_format_time.NULL <- function(x){ x } +#' @rdname sf_format_time #' @export sf_format_time.AsIs <- function(x){ - x + if(length(class(x)[-match("AsIs", class(x))]) == 0){ + class(x) <- NULL + } else { + class(x) <- class(x)[-match("AsIs", class(x))] + } + sf_format_time(x) } diff --git a/R/utils-httr.R b/R/utils-httr.R index 4458ee69..b8080560 100644 --- a/R/utils-httr.R +++ b/R/utils-httr.R @@ -27,9 +27,11 @@ #' Generic implementation of HTTP methods with retries and authentication #' -#' @param verb string; The name of HTTP verb to execute #' @importFrom httr RETRY status_code config add_headers #' @importFrom stats runif +#' @param verb string; The name of HTTP verb to execute +#' @return The last response. Note that if the request doesn't succeed after +#' \code{times} then the request will fail and return the response. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -60,6 +62,7 @@ VERB_n <- function(verb) { #' GETs with retries and authentication #' #' @importFrom httr GET +#' @return A \code{response()} object as defined by the \code{httr} package. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -67,6 +70,8 @@ rGET <- VERB_n("GET") #' POSTs with retries and authentication #' +#' @importFrom httr POST +#' @return A \code{response()} object as defined by the \code{httr} package. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -74,6 +79,8 @@ rPOST <- VERB_n("POST") #' PATCHs with retries and authentication #' +#' @importFrom httr PATCH +#' @return A \code{response()} object as defined by the \code{httr} package. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -81,6 +88,8 @@ rPATCH <- VERB_n("PATCH") #' PUTs with retries and authentication #' +#' @importFrom httr PUT +#' @return A \code{response()} object as defined by the \code{httr} package. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -88,6 +97,8 @@ rPUT <- VERB_n("PUT") #' DELETEs with retries and authentication #' +#' @importFrom httr DELETE +#' @return A \code{response()} object as defined by the \code{httr} package. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -97,6 +108,8 @@ rDELETE <- VERB_n("DELETE") #' #' Assuming the error code is less than 500, this function will return the #' +#' @param x \code{response()}; a response that indicates an error +#' @return \code{list}; a list containing the error code and message for printing. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -126,6 +139,9 @@ parse_error_code_and_message <- function(x){ #' #' @importFrom httr content http_error #' @importFrom xml2 as_list xml_find_first +#' @param x \code{response()}; a response from an HTTP request +#' @return \code{logical}; return \code{FALSE} if the function finishes without +#' detecting an error, otherwise stop the function call. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -153,16 +169,18 @@ catch_errors <- function(x){ #' Execute a non-paginated REST API call to list items #' +#' @importFrom purrr map_df +#' @importFrom dplyr as_tibble tibble +#' @importFrom readr col_guess type_convert +#' @importFrom httr content +#' @param url \code{character}; a valid REST API URL (as a string) #' @template as_tbl #' @param records_root \code{character} or \code{integer}; an index or string that #' identifies the element in the parsed list which contains the records returned #' by the API call. By default, this argument is \code{NULL}, which means that #' each element in the list is an individual record. #' @template verbose -#' @importFrom purrr map_df -#' @importFrom dplyr as_tibble tibble -#' @importFrom readr col_guess type_convert -#' @importFrom httr content +#' @return \code{tbl_df} or \code{list} of data depending on what was requested. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -199,6 +217,7 @@ sf_rest_list <- function(url, #' Function to build a proxy object to pass along with httr requests #' #' @importFrom httr use_proxy +#' @return an \code{httr} proxy object #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export diff --git a/R/utils-input-validation.R b/R/utils-input-validation.R index 610ad4e4..44825c83 100644 --- a/R/utils-input-validation.R +++ b/R/utils-input-validation.R @@ -102,6 +102,11 @@ message_w_errors_listed <- function(main_text = "Consider the following:", #' Validate the input for an operation #' +#' @template input_data +#' @template operation +#' @return the input data validated and formatted according to the specified +#' operation. This allows more flexibility for the user while ensuring that all +#' inputs are formatted as expected by the target APIs and operations. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export diff --git a/R/utils-org.R b/R/utils-org.R index 1b6038cf..9ef5419b 100644 --- a/R/utils-org.R +++ b/R/utils-org.R @@ -1,7 +1,7 @@ #' Return Current User Info #' #' @description -#' `r lifecycle::badge("maturing")` +#' `r lifecycle::badge("stable")` #' #' Retrieves personal information for the user associated with the current session. #' @@ -775,7 +775,7 @@ sf_merge <- function(master_id, #' Get Deleted Records from a Timeframe #' #' @description -#' `r lifecycle::badge("maturing")` +#' `r lifecycle::badge("stable")` #' #' Retrieves the list of individual records that have been deleted within the given #' timespan for the specified object. @@ -847,7 +847,7 @@ sf_get_deleted <- function(object_name, #' Get Updated Records from a Timeframe #' #' @description -#' `r lifecycle::badge("maturing")` +#' `r lifecycle::badge("stable")` #' #' Retrieves the list of individual records that have been inserted or updated #' within the given timespan in the specified object. diff --git a/R/utils-query.R b/R/utils-query.R index 4acdb5ab..e8642f6f 100644 --- a/R/utils-query.R +++ b/R/utils-query.R @@ -260,7 +260,10 @@ drop_attributes_recursively <- function(x, #' @importFrom purrr map modify_if #' @importFrom rlist list.flatten #' @importFrom utils head tail -#' @param x \code{list}; a list of xml content parsed into a list by xml2 +#' @param x \code{list}; a list of XML content parsed into a list by xml2 +#' @return \code{character}; a named vector of strings from the parsed XML. Nested +#' elements have their hierarchy represented by a period between the element names +#' at each level. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -468,9 +471,10 @@ extract_records_from_xml_nodeset_of_records <- function(x, #' bound together to return one complete \code{tbl_df} of the query result for #' that parent record. #' +#' @importFrom xml2 xml_find_all xml_remove #' @param x \code{xml_node}; a \code{xml_node} from an xml2 parsed response #' representing one individual parent query record. -#' @importFrom xml2 xml_find_all xml_remove +#' @return \code{tbl_df} of the query result for that parent record. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -574,6 +578,8 @@ list_extract_parent_and_child_result <- function(x){ #' the query recordset, that can be joined with its corresponding child records. #' @param child_df_list \code{list} of \code{tbl_df}; a list of child records that #' is the same length as the number of rows in the parent_df. +#' @return \code{tbl_df}; a data frame of parent data replicated for each child +#' record in the corresponding list. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -674,7 +680,7 @@ records_list_to_tbl <- function(x, #' @export bind_query_resultsets <- function(resultset, next_records){ - deprecate_warn("0.2.2", "salesforcer::bind_query_resultsets()", "safe_bind_rows()", + deprecate_warn("0.2.2", "salesforcer::bind_query_resultsets()", "salesforcer::safe_bind_rows()", details = paste0("Consider safe_bind_rows() which silently combines ", "all columns regardless if there are mixed datatypes ", "in a single column.")) @@ -696,10 +702,8 @@ bind_query_resultsets <- function(resultset, next_records){ "had different datatypes than prior records in the following columns:", "\n - %s\n", "\n", - "Consider setting `bind_using_character_cols=TRUE` to cast the data to ", - "character so that `bind_rows()` between pages will succeed and setting ", - "`guess_types=TRUE` which uses readr to determine the datatype based on ", - "values in the column."), + "Consider setting `guess_types=FALSE` and manually examinig ", + "why the datatypes are varying in a particular column."), paste0(mismatched_warn_str, collapse="\n - ")) , call. = FALSE ) @@ -733,12 +737,13 @@ sf_reorder_cols <- function(df){ contains(".")) } -#' Reorder resultset columns to prioritize \code{sObject} and \code{Id} +#' Parse resultset columns to a known datatype in R #' #' This function accepts a \code{tbl_df} with columns rearranged. #' #' @importFrom dplyr mutate across -#' @importFrom readr type_convert cols col_guess +#' @importFrom anytime anytime anydate +#' @importFrom readr type_convert cols col_guess locale #' @param df \code{tbl_df}; the data frame to rearrange columns in #' @return \code{tbl_df} the formatted data frame #' @note This function is meant to be used internally. Only use when debugging. @@ -746,19 +751,32 @@ sf_reorder_cols <- function(df){ #' @export sf_guess_cols <- function(df, guess_types=TRUE, dataType=NULL){ if(guess_types){ - if(is.null(dataType) || any(is.na(dataType)) || (length(dataType)== 0)){ + if(is.null(dataType) || any(is.na(dataType)) || (length(dataType) == 0)){ df <- df %>% - type_convert(col_types = cols(.default = col_guess())) + type_convert(col_types = cols(.default = col_guess()), locale=locale(tz="UTC")) } else { col_spec <- sf_build_cols_spec(dataType) - # if numeric but contains Salesforce "-" then preemptively change to NA + # if numeric Salesforce will flag N/A as "-" so we need to preemptively change to NA + # TODO: Does it use "-" for NA or zero? Or both? if(grepl('i|n', col_spec)){ numeric_col_idx <- which(strsplit(col_spec, split=character(0))[[1]] %in% c("i", "n")) df <- df %>% mutate(across(all_of(numeric_col_idx), ~ifelse(.x == "-", NA_character_, .x))) } - df <- df %>% - type_convert(col_types = col_spec) + # Salesforce returns dates and datetimes in UTC but sometimes as YYYY-MM-DD + # or MM/DD/YYYY in the case of reports, so we will convert using the + # anytime package rather than trusting type_convert's behavior + if(grepl('D', col_spec)){ + date_col_idx <- which(strsplit(col_spec, split=character(0))[[1]] == "D") + df <- df %>% + mutate(across(all_of(date_col_idx), ~as.character(anydate(.x, tz="UTC", asUTC=TRUE)))) + } + if(grepl('T', col_spec)){ + datetime_col_idx <- which(strsplit(col_spec, split=character(0))[[1]] == "T") + df <- df %>% + mutate(across(all_of(datetime_col_idx), ~as.character(anytime(.x, tz="UTC", asUTC=TRUE)))) + } + df <- df %>% type_convert(col_types = col_spec, locale=locale(tz="UTC")) } } return(df) diff --git a/R/utils-report.R b/R/utils-report.R index 40bc3b68..6b4856c5 100644 --- a/R/utils-report.R +++ b/R/utils-report.R @@ -4,6 +4,7 @@ #' selects either the value or label for the report columns to turn into a one #' row \code{tbl_df} that will usually be bound to the other rows in the report #' +#' @importFrom lifecycle deprecated is_present deprecate_warn #' @importFrom purrr map pluck #' @importFrom dplyr as_tibble #' @importFrom vctrs vec_as_names @@ -22,19 +23,25 @@ format_report_row <- function(x, labels = TRUE, guess_types = TRUE, - bind_using_character_cols = FALSE){ + bind_using_character_cols = deprecated()){ stopifnot(is.list(x), names(x) == "dataCells") + + if(is_present(bind_using_character_cols)) { + deprecate_warn("1.0.0", + "salesforcer::format_report_row(bind_using_character_cols)", + details = paste0("The `bind_using_character_cols` functionality ", + "will always be `TRUE` going forward. Per the ", + "{readr} package, we have to read as character ", + "and then invoke `type_convert()` in order to ", + "use all values in a column to guess its type.")) + } + element_name <- if(labels) "label" else "value" + x$dataCells %>% map(~pluck(.x, element_name)) %>% modify_if(~(length(.x) == 0), .f=function(x){return(NA)}) %>% - { - if(!guess_types | bind_using_character_cols){ - map(., as.character) - } else { - . - } - } %>% + map(., as.character) %>% as_tibble(.name_repair = ~vec_as_names(names = paste0("v", seq_len(length(.))), repair = "unique", quiet = TRUE)) @@ -46,6 +53,7 @@ format_report_row <- function(x, #' parses it to return a single \code{tbl_df} representing the detail rows and #' columns of the report without any filters, aggregates, or totals. #' +#' @importFrom lifecycle deprecated is_present deprecate_warn #' @importFrom stats setNames #' @importFrom purrr map pluck #' @importFrom dplyr as_tibble @@ -66,7 +74,17 @@ parse_report_detail_rows <- function(content, fact_map_key = "T!T", labels = TRUE, guess_types = TRUE, - bind_using_character_cols = FALSE){ + bind_using_character_cols = deprecated()){ + + if(is_present(bind_using_character_cols)) { + deprecate_warn("1.0.0", + "salesforcer::parse_report_detail_rows(bind_using_character_cols)", + details = paste0("The `bind_using_character_cols` functionality ", + "will always be `TRUE` going forward. Per the ", + "{readr} package, we have to read as character ", + "and then invoke `type_convert()` in order to ", + "use all values in a column to guess its type.")) + } # create a boolean that will be set whenever an offending issue is identified # that can be used to stop the function execution only after all of the checks @@ -124,8 +142,7 @@ parse_report_detail_rows <- function(content, drop_empty_recursively() %>% map_df(format_report_row, labels = labels, - guess_types = guess_types, - bind_using_character_cols = bind_using_character_cols) %>% + guess_types = guess_types) %>% set_names(nm = result_colnames) } diff --git a/R/utils-xml.R b/R/utils-xml.R index 12269d53..1d1998b4 100644 --- a/R/utils-xml.R +++ b/R/utils-xml.R @@ -54,7 +54,7 @@ xmlToList2 <- function(node){ #' @importFrom purrr modify_if #' @importFrom utils capture.output #' @param this_node \code{xml_node}; to be parsed out -#' @return \code{data.frame} parsed from the supplied xml +#' @return \code{tbl_df} parsed from the supplied XML #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -78,7 +78,10 @@ xml_nodeset_to_df <- function(this_node){ #' #' @importFrom XML newXMLNode xmlValue<- #' @param soap_headers \code{list}; any number of SOAP headers -#' @return a XML document +#' @param metadata_ns \code{logical}; an indicator of whether to use the namespaces +#' required by the Metadata API or the default ones. +#' @return \code{xmlNode}; an XML object containing just the header portion of the +#' request #' @references \url{https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/soap_headers.htm} #' @note This function is meant to be used internally. Only use when debugging. #' Any of the following SOAP headers are allowed: @@ -138,12 +141,26 @@ 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]), - 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) + option_name <- names(soap_headers)[i] + opt_node <- newXMLNode(paste0(ns_prefix, ":", option_name), parent=header_node) + # process OwnerChangeOptions differently because it can be a list of multiple + # different options all put under the OwnerChangeOptions node + if (option_name == "OwnerChangeOptions"){ + options_spec <- soap_headers[[i]]$options + for(j in 1:length(options_spec)){ + this_node <- newXMLNode(paste0(ns_prefix, ":", "options"), parent=opt_node) + for(k in 1:length(options_spec[[j]])){ + this_node2 <- newXMLNode(paste0(ns_prefix, ":", names(options_spec[[j]])[k]), + as.character(options_spec[[j]][[k]]), + parent=this_node) + } + } + } else { + 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) + } } } } @@ -158,14 +175,15 @@ make_soap_xml_skeleton <- function(soap_headers=list(), metadata_ns=FALSE){ #' @param input_data a \code{data.frame} of data to fill the XML body #' @template operation #' @template object_name -#' @param fields \code{character}; one or more strings indicating the fields to be returned -#' on the records +#' @param fields \code{character}; one or more strings indicating the fields to +#' be returned on the records #' @template external_id_fieldname #' @param root_name \code{character}; the name of the root node if created #' @param ns named vector; a collection of character strings indicating the namespace #' definitions of the root node if created -#' @param root \code{XMLNode}; a node to be used as the root -#' @return a XML document +#' @param root \code{xmlNode}; an XML node to be used as the root +#' @return \code{xmlNode}; an XML node with the complete XML built using the root +#' and the input data in the format needed for the operation. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -314,8 +332,9 @@ build_soap_xml_from_list <- function(input_data, #' @param root_name \code{character}; the name of the root node if created #' @param ns named vector; a collection of character strings indicating the namespace #' definitions of the root node if created -#' @param root \code{XMLNode}; a node to be used as the root -#' @return A XML document with the sublist data added +#' @param root \code{xmlNode}; an XML node to be used as the root +#' @return \code{xmlNode}; an XML node with the input data added as needed for the +#' Metadata API and its objects. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -357,9 +376,10 @@ build_metadata_xml_from_list <- function(input_data, #' #' @importFrom XML newXMLNode xmlValue<- #' @param input_data \code{list}; data to be appended -#' @param root \code{XMLNode}; a node to be used as the root +#' @param root \code{xmlNode}; an XML node to be used as the root +#' @return \code{xmlNode}; an XML node constructed into a manifest data required +#' by the Bulk APIs for handling binary attachment data. #' @references \url{https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/binary_create_request_file.htm} -#' @return A XML document with the sublist manifest data added #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export diff --git a/R/utils.R b/R/utils.R index b9889d23..9c519a99 100644 --- a/R/utils.R +++ b/R/utils.R @@ -1,5 +1,6 @@ #' Return the package's .state environment variable #' +#' @return \code{list}; a list of values stored in the package's .state environment variable #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -12,7 +13,7 @@ salesforcer_state <- function(){ #' This function determines whether the system running the R code #' is Windows, Mac, or Linux #' -#' @return A character string +#' @return \code{character}; a string indicating the current operating system. #' @examples #' \dontrun{ #' get_os() @@ -63,6 +64,14 @@ patched_tempdir <- function(){ #' Return NA if NULL #' +#' A helper function to convert NULL values in API responses to a value of NA +#' which is allowed in data frames. Oftentimes, a NULL value creates issues when +#' binding and building data frames from parsed output, so we need to switch to NA. +#' +#' @param x a value, typically a single element or a list to switch to NA if +#' its value appears to be NULL. +#' @return the original value of parameter \code{x} or \code{NA} if the value +#' meets the criteria to be considered NULL. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -80,12 +89,29 @@ merge_null_to_na <- function(x){ #' Write a CSV file in format acceptable to Salesforce APIs #' +#' @importFrom lifecycle deprecate_warn is_present deprecated #' @importFrom readr write_csv +#' @param x \code{tbl_df}; a data frame object to save as a CSV +#' @param file A file or connection to write to. +#' @param path `r lifecycle::badge("deprecated")` +#' use the `file` argument instead. +#' @return the input \code{x} invisibly. This function is called for its +#' side-effect of creating a CSV file at the specified location using the format +#' required by Salesforce. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export -sf_write_csv <- function(x, path){ - write_csv(x=x, path=path, na="#N/A") +sf_write_csv <- function(x, file, path=deprecated()){ + if(is_present(path)) { + deprecate_warn("1.0.0", + "salesforcer::sf_write_csv(path = )", + "salesforcer::sf_write_csv(file = )", + details = paste0("The readr package changed the name of the `path` ", + "argument to `file` in its v1.4.0 release.") + ) + file <- path + } + write_csv(x=x, file=file, na="#N/A") } #' Remove NA Columns Created by Empty Related Entity Values @@ -94,9 +120,11 @@ sf_write_csv <- function(x, path){ #' in the resultset and try to exclude an additional completely blank column #' created by records that don't have a relationship at all in that related entity. #' +#' @importFrom dplyr select one_of #' @param dat data; a \code{tbl_df} or \code{data.frame} of a returned resultset #' @template api_type -#' @importFrom dplyr select one_of +#' @return \code{tbl_df}; the passed in data, but with the object columns removed +#' that are empty links to other objects. #' @keywords internal #' @export remove_empty_linked_object_cols <- function(dat, api_type = c("SOAP", "REST")){ @@ -131,6 +159,9 @@ remove_empty_linked_object_cols <- function(dat, api_type = c("SOAP", "REST")){ #' Try to Guess the Object if User Does Not Specify for Bulk Queries #' +#' @template soql +#' @return \code{character}; a string parsed from the input that represents the +#' object name that the query appears to target. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -147,6 +178,10 @@ guess_object_name_from_soql <- function(soql){ #' Format Headers for Printing #' +#' @param request_headers \code{list}; a list of values from the API request or +#' response that represent the headers of the call +#' @return \code{character}; a string constructed from the input that is easier +#' to read when we print it out #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export @@ -159,6 +194,16 @@ format_headers_for_verbose <- function(request_headers){ #' Format Verbose Call #' #' @importFrom jsonlite prettify +#' @param method \code{character}; the type of HTTP method invoked (e.g., POST, +#' PUT, DELETE, etc.). +#' @param url \code{character}; the URL that the request was sent to +#' @param headers \code{character}; the set of header options set on the request +#' @param body \code{character}; the body of the request. +#' @param auto_unbox \code{logical}, an indicator of whether to parse vectors of +#' of length 1 into a single character string, rather than a list. +#' @param ... additional arguments passed on to \code{\link[jsonlite]{toJSON}}. +#' @return \code{NULL} invisibly, because this function is intended for the +#' side-effect of printing out the details of an HTTP call. #' @note This function is meant to be used internally. Only use when debugging. #' @keywords internal #' @export diff --git a/R/zzz.R b/R/zzz.R index 8f039556..2535ccc2 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -2,7 +2,7 @@ op <- options() op.salesforcer <- list( - salesforcer.api_version = "48.0", + salesforcer.api_version = "52.0", salesforcer.login_url = "https://login.salesforce.com", salesforcer.proxy_url = "", salesforcer.proxy_port = NULL, diff --git a/README.Rmd b/README.Rmd index 642000ca..9e27156b 100644 --- a/README.Rmd +++ b/README.Rmd @@ -18,7 +18,7 @@ options(tibble.print_min = 5L, tibble.print_max = 5L) [![R Build Status](https://github.com/stevenmmortimer/salesforcer/workflows/R-CMD-check/badge.svg)](https://github.com/stevenmmortimer/salesforcer/actions?workflow=R-CMD-check) [![CRAN Status](https://www.r-pkg.org/badges/version/salesforcer)](https://cran.r-project.org/package=salesforcer) -[![Lifecycle: Maturing](https://img.shields.io/badge/lifecycle-maturing-blue.svg)](https://www.tidyverse.org/lifecycle/#maturing) +[![Lifecycle: Stable](https://img.shields.io/badge/lifecycle-stable-brightgreen.svg)](https://lifecycle.r-lib.org/articles/stages.html#stable) [![Monthly Downloads](https://cranlogs.r-pkg.org/badges/last-month/salesforcer)](https://cran.r-project.org/package=salesforcer) [![Coverage Status](https://codecov.io/gh/stevenmmortimer/salesforcer/branch/main/graph/badge.svg)](https://codecov.io/gh/stevenmmortimer/salesforcer?branch=main) @@ -69,7 +69,7 @@ Package features include: ## Installation ```{r, eval = FALSE} -# install the current CRAN version (0.2.2) +# install the current CRAN version (1.0.0) install.packages("salesforcer") # or get the development version on GitHub @@ -221,7 +221,7 @@ deleted_records 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) +and [Bulk 2.0](https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/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. diff --git a/README.md b/README.md index 1da63b18..46a1ed4b 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Status](https://github.com/stevenmmortimer/salesforcer/workflows/R-CMD-check/bad [![CRAN Status](https://www.r-pkg.org/badges/version/salesforcer)](https://cran.r-project.org/package=salesforcer) [![Lifecycle: -Maturing](https://img.shields.io/badge/lifecycle-maturing-blue.svg)](https://www.tidyverse.org/lifecycle/#maturing) +Stable](https://img.shields.io/badge/lifecycle-stable-brightgreen.svg)](https://lifecycle.r-lib.org/articles/stages.html#stable) [![Monthly Downloads](https://cranlogs.r-pkg.org/badges/last-month/salesforcer)](https://cran.r-project.org/package=salesforcer) [![Coverage @@ -69,7 +69,7 @@ Package features include: ## Installation ``` r -# install the current CRAN version (0.2.2) +# install the current CRAN version (1.0.0) install.packages("salesforcer") # or get the development version on GitHub @@ -242,7 +242,7 @@ 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) +[Bulk 2.0](https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/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. diff --git a/cran-comments.md b/cran-comments.md index 9581d606..f900ffd2 100644 --- a/cran-comments.md +++ b/cran-comments.md @@ -1,30 +1,49 @@ # Release summary +## Note for CRAN Maintainers upon Submission of salesforcer 1.0.0 + +Per the request of Julia Haider, I have added \value{} specs to the .Rd files +for all exported methods and explained those values in detail. + +In addition, she requested that I explain how the issues which caused this +package to be archived have now been resolved. On June 8, 2021, CRAN archived the +{RForcecom} package. Tests and vignettes in this package referenced {RForcecom} +and it was listed in this package's DESCRIPTION file under 'Suggests'. On June 9, +2021, CRAN sent an email that this package had failing check results. The +failures were due to RForcecom having been archived on CRAN. To resolve, I +removed all executing references to the RForcecom library in the code, because +I cannot anticipate whether the author of the package will restore it to CRAN. I +was not able to complete this work by the stated deadline (June 23) because I was +traveling without connection to the tools required to make the necessary changes. +As such, CRAN archived this package. + ## Test environments -* local mac OS install, R-release 4.0.2 -* ubuntu 16.04 (on github actions), R-release, R 4.0.2 -* mac OS 10.15.5 (on github actions) R-release, R 4.0.2 -* Microsoft Windows Server 2019 10.0.17763 (on github actions) R-release, R 4.0.2 -* win-builder (R-devel) +* Local Mac OS install, R-release 4.0.2 +* Ubuntu 16.04 (on GitHub Actions), R-release, R 4.1.0 +* Mac OS 10.15.5 (on GitHub Actions) R-release, R 4.1.0 +* Microsoft Windows Server 2019 10.0.17763 (on GitHub Actions) R-release, R 4.1.0 +* win-builder (R-release 4.1.0) ## R CMD check results -checking CRAN incoming feasibility ... NOTE - Maintainer: 'Steven M. Mortimer ' +* checking CRAN incoming feasibility ... NOTE +Maintainer: 'Steven M. Mortimer ' -New maintainer: - Steven M. Mortimer -Old maintainer(s): - Steven M. Mortimer +New submission -0 errors v | 0 warnings v | 1 note x +Package was archived on CRAN ----- +Possibly mis-spelled words in DESCRIPTION: + APIs (2:42, 5:64, 9:16) + JSON (9:59) -## revdepcheck results +CRAN repository db overrides: + X-CRAN-Comment: Archived on 2021-06-23 as check problems were not + corrected in time. -We checked 0 reverse dependencies, comparing R CMD check results across CRAN and dev versions of this package. +0 errors v | 0 warnings v | 1 note x + +## revdepcheck results - * We saw 0 new problems - * We failed to check 0 packages +Not done for this version because the prior version was removed from CRAN. diff --git a/docs/404.html b/docs/404.html index 765836bf..cc97c43f 100644 --- a/docs/404.html +++ b/docs/404.html @@ -95,7 +95,7 @@ salesforcer - 0.2.2 + 1.0.0 @@ -103,7 +103,7 @@