Join GitHub today
GitHub is home to over 50 million developers working together to host and review code, manage projects, and build software together.
Sign up| # https://resources.docs.salesforce.com/226/latest/en-us/sfdc/pdf/salesforce_analytics_rest_api.pdf | |
| #' List reports | |
| #' | |
| #' @description | |
| #' `r lifecycle::badge("experimental")` | |
| #' | |
| #' Displays a list of full list of reports based on the \code{Report} object. If | |
| #' \code{recent} is up to 200 tabular, matrix, or summary reports that you | |
| #' recently viewed. To get additional details on reports by format, name, and other | |
| #' fields, use a SOQL query on the Report object. | |
| #' | |
| #' @importFrom dplyr rename_with mutate expr | |
| #' @importFrom purrr transpose | |
| #' @param recent \code{logical}; an indicator of whether to return the 200 most | |
| #' recently viewed reports or to invoke a query on the \code{Report} object to | |
| #' return all reports in the Org. By default, this argument is set to \code{FALSE} | |
| #' meaning that all of the reports, not just the most recently viewed reports | |
| #' are returned. Note that the default behavior of the reports list endpoint in | |
| #' the Reports and Dashboards REST API is only the most recently viewed up to | |
| #' 200 reports. | |
| #' @template as_tbl | |
| #' @template verbose | |
| #' @return \code{tbl_df} by default, or a \code{list} depending on the value of | |
| #' argument \code{as_tbl} | |
| #' @note This function will only return up to 200 of recently viewed reports when the | |
| #' \code{recent} argument is set to \code{TRUE}. For a complete details you must | |
| #' use \code{\link{sf_query}} on the report object. | |
| #' @family Report functions | |
| #' @section Salesforce Documentation: | |
| #' \itemize{ | |
| #' \item \href{https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_recentreportslist.htm}{Documentation} | |
| #' \item \href{https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_list_recentreports.htm#example_recent_reportslist}{Example} | |
| #' } | |
| #' @examples | |
| #' \dontrun{ | |
| #' # to return all possible reports, which is queried from the Report object | |
| #' reports <- sf_list_reports() | |
| #' | |
| #' # return the results as a list | |
| #' reports_as_list <- sf_list_reports(as_tbl=FALSE) | |
| #' | |
| #' # return up to 200 recently viewed reports | |
| #' all_reports <- sf_list_reports(recent=TRUE) | |
| #' } | |
| #' @export | |
| sf_list_reports <- function(recent=FALSE, as_tbl=TRUE, verbose=FALSE){ | |
| if(recent){ | |
| this_url <- make_reports_list_url() | |
| resultset <- sf_rest_list(url=this_url, as_tbl=as_tbl, verbose=verbose) | |
| } else { | |
| resultset = sf_query("SELECT Id, Name FROM Report") | |
| # add columns to match the actual output of report list | |
| report_base_url <- "/services/data/v48.0/analytics/reports" | |
| resultset <- resultset %>% | |
| rename_with(tolower) %>% | |
| mutate(url = sprintf('%s/%s', report_base_url, expr("id")), | |
| describeUrl = sprintf('%s/%s/%s', report_base_url, expr("id"), "describe"), | |
| fieldsUrl = sprintf('%s/%s/%s', report_base_url, expr("id"), "fields"), | |
| instancesUrl = sprintf('%s/%s/%s', report_base_url, expr("id"), "instances")) | |
| # convert the tibble returned by the query back into a list | |
| if(!as_tbl){ | |
| resultset <- resultset %>% transpose() | |
| } | |
| } | |
| if(as_tbl){ | |
| # bring id and name up front | |
| resultset <- resultset %>% | |
| sf_reorder_cols() %>% | |
| sf_guess_cols() | |
| } | |
| return(resultset) | |
| } | |
| #' List report filter operators | |
| #' | |
| #' @description | |
| #' `r lifecycle::badge("experimental")` | |
| #' | |
| #' Use the Filter Operators API to get information about which filter operators are | |
| #' available for reports and dashboards. The Filter Operators API is available in | |
| #' API version 40.0 and later. | |
| #' | |
| #' @template as_tbl | |
| #' @template verbose | |
| #' @return \code{tbl_df} by default, or a \code{list} depending on the value of | |
| #' argument \code{as_tbl} | |
| #' @family Report functions | |
| #' @section Salesforce Documentation: | |
| #' \itemize{ | |
| #' \item \href{https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/analytics_api_filteroperators_reference_resource.htm}{Documentation} | |
| #' \item \href{https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/analytics_api_filteroperators_reference_list.htm}{Example} | |
| #' } | |
| #' @examples \dontrun{ | |
| #' report_filters <- sf_list_report_filter_operators() | |
| #' unique_supported_fields <- report_filters %>% distinct(supported_field_type) | |
| #' | |
| #' # operators to filter a picklist field | |
| #' picklist_field_operators <- report_filters %>% filter(supported_field_type == "picklist") | |
| #' } | |
| #' @export | |
| sf_list_report_filter_operators <- function(as_tbl=TRUE, verbose=FALSE){ | |
| this_url <- make_report_filter_operators_list_url() | |
| resultset <- sf_rest_list(url=this_url, as_tbl=FALSE, verbose=verbose) | |
| if(as_tbl){ | |
| resultset <- lapply(resultset, FUN=function(x){x %>% map_df(flatten_tbl_df)}) | |
| resultset <- safe_bind_rows(resultset, idcol="supported_field_type") | |
| } | |
| return(resultset) | |
| } | |
| #' Get a list of report fields | |
| #' | |
| #' @description | |
| #' `r lifecycle::badge("experimental")` | |
| #' | |
| #' The Report Fields resource returns report fields available for specified reports. | |
| #' Use the resource to determine the best fields for use in dashboard filters by | |
| #' seeing which fields different source reports have in common. Available in API | |
| #' version 40.0 and later. | |
| #' | |
| #' @template report_id | |
| #' @param intersect_with \code{character} a vector of unique report IDs. This is | |
| #' helpful in determining the best fields for use in dashboard filters by seeing | |
| #' which fields different source reports have in common. If this argument is left | |
| #' empty, then the function returns a list of all possible report fields. | |
| #' Otherwise, returns a list of fields that specified reports share. | |
| #' @template verbose | |
| #' @return \code{list} representing the 4 different field report properties: | |
| #' \describe{ | |
| #' \item{displayGroups}{Fields available when adding a filter.} | |
| #' \item{equivalentFields}{Fields available for each specified report. Each object in this array is a list of common fields categorized by report type.} | |
| #' \item{equivalentFieldIndices}{Map of each field’s API name to the index of the field in the \code{equivalentFields} array.} | |
| #' \item{mergedGroups}{Merged fields.} | |
| #' } | |
| #' @family Report functions | |
| #' @section Salesforce Documentation: | |
| #' \itemize{ | |
| #' \item \href{https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_get_fields.htm}{Documentation} | |
| #' } | |
| #' @examples | |
| #' \dontrun{ | |
| #' # first, grab all possible reports in your Org | |
| #' all_reports <- sf_query("SELECT Id, Name FROM Report") | |
| #' | |
| #' # second, get the id of the report to check fields on | |
| #' this_report_id <- all_reports$Id[1] | |
| #' | |
| #' # third, pull that report and intersect its fields with up to three other reports | |
| #' fields <- sf_list_report_fields(this_report_id, intersect_with=head(all_reports[["Id"]],3)) | |
| #' } | |
| #' @export | |
| sf_list_report_fields <- function(report_id, | |
| intersect_with = c(character(0)), | |
| verbose=FALSE){ | |
| this_url <- make_report_fields_url(report_id) | |
| request_body <- list(intersectWith=I(intersect_with)) | |
| httr_response <- rPOST(url = this_url, | |
| body = request_body, | |
| encode = "json") | |
| if(verbose){ | |
| make_verbose_httr_message(httr_response$request$method, | |
| httr_response$request$url, | |
| httr_response$request$headers) | |
| } | |
| catch_errors(httr_response) | |
| response_parsed <- content(httr_response, as="parsed", encoding="UTF-8") | |
| return(response_parsed) | |
| } | |
| #' List report types | |
| #' | |
| #' @description | |
| #' `r lifecycle::badge("experimental")` | |
| #' | |
| #' Return a list of report types. | |
| #' | |
| #' @template as_tbl | |
| #' @template verbose | |
| #' @return \code{tbl_df} by default, or a \code{list} depending on the value of | |
| #' argument \code{as_tbl} | |
| #' @family Report functions | |
| #' @section Salesforce Documentation: | |
| #' \itemize{ | |
| #' \item \href{https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/analytics_api_reporttypes_reference_list.htm}{Documentation} | |
| #' } | |
| #' @examples | |
| #' \dontrun{ | |
| #' report_types <- sf_list_report_types() | |
| #' unique_report_types <- report_types %>% select(reportTypes.type) | |
| #' | |
| #' # return the results as a list | |
| #' reports_as_list <- sf_list_report_types(as_tbl=FALSE) | |
| #' } | |
| #' @export | |
| sf_list_report_types <- function(as_tbl=TRUE, verbose=FALSE){ | |
| this_url <- make_report_types_list_url() | |
| resultset <- sf_rest_list(url=this_url, as_tbl=as_tbl, verbose=verbose) | |
| if(as_tbl){ | |
| resultset <- resultset %>% unnest_col(col="reportTypes") | |
| } | |
| return(resultset) | |
| } | |
| #' Describe a report type | |
| #' | |
| #' @description | |
| #' `r lifecycle::badge("experimental")` | |
| #' | |
| #' Return metadata about a report type. | |
| #' | |
| #' @param report_type \code{character}; a character representing the type of | |
| #' report to retrieve the metadata information on. A list of valid report types | |
| #' that can be described using this function will be available in the | |
| #' \code{reportTypes.type} column of results returned \link{sf_list_report_types}. | |
| #' (e.g. \code{AccountList}, \code{AccountContactRole}, \code{OpportunityHistory}, | |
| #' etc.) | |
| #' @template verbose | |
| #' @return \code{list} containing up to 4 properties that describe the report: | |
| #' \describe{ | |
| #' \item{attributes}{Report type along with the URL to retrieve common objects and | |
| #' joined metadata.} | |
| #' \item{reportMetadata}{Unique identifiers for groupings and summaries.} | |
| #' \item{reportTypeMetadata}{Fields in each section of a report type plus filter information for those fields.} | |
| #' \item{reportExtendedMetadata}{Additional information about summaries and groupings.} | |
| #' } | |
| #' @family Report functions | |
| #' @section Salesforce Documentation: | |
| #' \itemize{ | |
| #' \item \href{https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/analytics_api_reporttypes_reference_reporttype.htm}{Documentation} | |
| #' } | |
| #' @examples \dontrun{ | |
| #' reports <- sf_list_report_types() | |
| #' unique_report_types <- reports %>% distinct(reportTypes.type) | |
| #' | |
| #' # first unique report type | |
| #' unique_report_types[[1,1]] | |
| #' | |
| #' # describe that report type | |
| #' described_report <- sf_describe_report_type(unique_report_types[[1,1]]) | |
| #' } | |
| #' @export | |
| sf_describe_report_type <- function(report_type, verbose=FALSE){ | |
| this_url <- make_report_type_describe_url(report_type) | |
| resultset <- sf_rest_list(url=this_url, as_tbl=FALSE, verbose=verbose) | |
| return(resultset) | |
| } | |
| #' Describe a report | |
| #' | |
| #' @description | |
| #' `r lifecycle::badge("experimental")` | |
| #' | |
| #' Retrieves report, report type, and related metadata for a tabular, summary, | |
| #' or matrix report. | |
| #' | |
| #' @details \itemize{ | |
| #' \item Report metadata gives information about the report as a whole. Tells you such things as, the report type, format, the fields that are summaries, row or column groupings, filters saved to the report, and so on. | |
| #' \item Report type metadata tells you about all the fields available in the report type, those you can filter, and by what filter criteria. | |
| #' \item Report extended metadata tells you about the fields that are summaries, groupings, and contain record details in the report. | |
| #' } | |
| #' @template report_id | |
| #' @template verbose | |
| #' @return \code{list} containing up to 4 properties that describe the report: | |
| #' \describe{ | |
| #' \item{attributes}{Report type along with the URL to retrieve common objects and joined metadata.} | |
| #' \item{reportMetadata}{Unique identifiers for groupings and summaries.} | |
| #' \item{reportTypeMetadata}{Fields in each section of a report type plus filter information for those fields.} | |
| #' \item{reportExtendedMetadata}{Additional information about summaries and groupings.} | |
| #' } | |
| #' @family Report functions | |
| #' @section Salesforce Documentation: | |
| #' \itemize{ | |
| #' \item \href{https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_getbasic_reportmetadata.htm}{Documentation} | |
| #' \item \href{https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_get_reportmetadata.htm#example_report_getdescribe}{Example} | |
| #' } | |
| #' @examples | |
| #' \dontrun{ | |
| #' # pull a list of up to 200 recent reports | |
| #' # (for a full list you must use sf_query on the Report object) | |
| #' reports <- sf_list_reports() | |
| #' | |
| #' # id for the first report | |
| #' reports[[1,"id"]] | |
| #' | |
| #' # describe that report type | |
| #' described_report <- sf_describe_report_type(unique_report_types[[1,"id"]]) | |
| #' } | |
| #' @export | |
| sf_describe_report <- function(report_id, verbose=FALSE){ | |
| this_url <- make_report_describe_url(report_id) | |
| resultset <- sf_rest_list(url=this_url, as_tbl=FALSE, verbose=verbose) | |
| return(resultset) | |
| } | |
| #' Copy a report | |
| #' | |
| #' @description | |
| #' `r lifecycle::badge("experimental")` | |
| #' | |
| #' Creates a copy of a custom, standard, or public report by sending a POST | |
| #' request to the Report List resource. | |
| #' | |
| #' @template report_id | |
| #' @param name \code{character}; a user-specified name for the newly cloned report. | |
| #' If left \code{NULL}, then the new name will be the same name as the report being | |
| #' cloned appended with " = Copy" that is prefixed with a number if that name is | |
| #' not unique. It is highly recommended to provide a name, if possible. | |
| #' @template verbose | |
| #' @return \code{list} representing the newly cloned report with up to 4 properties | |
| #' that describe the report: | |
| #' \describe{ | |
| #' \item{attributes}{Report type along with the URL to retrieve common objects and | |
| #' joined metadata.} | |
| #' \item{reportMetadata}{Unique identifiers for groupings and summaries.} | |
| #' \item{reportTypeMetadata}{Fields in each section of a report type plus filter information for those fields.} | |
| #' \item{reportExtendedMetadata}{Additional information about summaries and groupings.} | |
| #' } | |
| #' @family Report functions | |
| #' @section Salesforce Documentation: | |
| #' \itemize{ | |
| #' \item \href{https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_clone_report.htm}{Documentation} | |
| #' } | |
| #' @examples | |
| #' \dontrun{ | |
| #' # only the 200 most recently viewed reports | |
| #' most_recent_reports <- sf_report_list() | |
| #' | |
| #' # all possible reports in your Org | |
| #' all_reports <- sf_query("SELECT Id, Name FROM Report") | |
| #' | |
| #' # id of the report to copy | |
| #' this_report_id <- all_reports$Id[1] | |
| #' | |
| #' # not providing a name appends " - Copy" to the name of the report being cloned | |
| #' report_details <- sf_copy_report(this_report_id) | |
| #' | |
| #' # example of providing new name to the copied report | |
| #' report_details <- sf_copy_report(this_report_id, "My New Copy of Report ABC") | |
| #' } | |
| #' @export | |
| sf_copy_report <- function(report_id, name=NULL, verbose=FALSE){ | |
| this_url <- make_report_copy_url(report_id) | |
| if(is.null(name)){ | |
| existing_reports <- sf_query("SELECT Id, Name FROM Report") | |
| # remove names reflecting a " - Copy" | |
| existing_reports$original_name <- gsub(" - Copy[0-9]{0,}$", "", existing_reports$Name) | |
| # which report is being requested to copy | |
| this_report <- existing_reports[existing_reports$Id == report_id,,drop=FALSE] | |
| # check that a report matching the supplied Id exists | |
| if(nrow(this_report) == 0){ | |
| stop(sprintf("No report found with Id: %s", report_id)) | |
| } | |
| # how many other reports have that same name (after dropping " - Copy")? | |
| report_name_cnt <- sum(existing_reports$original_name == this_report$original_name) | |
| if(report_name_cnt == 1){ | |
| name <- paste0(this_report$original_name, " - Copy") | |
| } else { | |
| name <- paste0(this_report$original_name, " - Copy", report_name_cnt - 1) | |
| } | |
| message(sprintf("Naming the new report: '%s'", name)) | |
| } | |
| this_url <- make_report_copy_url(report_id) | |
| httr_response <- rPOST(url = this_url, | |
| body = list(reportMetadata=list(name=name)), | |
| encode = "json") | |
| if(verbose){ | |
| make_verbose_httr_message(httr_response$request$method, | |
| httr_response$request$url, | |
| httr_response$request$headers) | |
| } | |
| catch_errors(httr_response) | |
| response_parsed <- content(httr_response, as="parsed", encoding="UTF-8") | |
| return(response_parsed) | |
| } | |
| #' Create a report | |
| #' | |
| #' @description | |
| #' `r lifecycle::badge("experimental")` | |
| #' | |
| #' Create a new report using a POST request. To create a report, you only have to | |
| #' specify a name and report type to create a new report; all other metadata properties | |
| #' are optional. It is recommended to use the metadata from existing reports pulled | |
| #' using \code{\link{sf_describe_report}} as a guide on how to specify the properties | |
| #' of a new report. | |
| #' | |
| #' @param name \code{character}; a user-specified name for the report. | |
| #' @param report_type \code{character}; a character representing the type of | |
| #' report to retrieve the metadata information on. A list of valid report types | |
| #' that can be created using this function will be available in the | |
| #' \code{reportTypes.type} column of results returned \link{sf_list_report_types}. | |
| #' (e.g. \code{AccountList}, \code{AccountContactRole}, \code{OpportunityHistory}, | |
| #' etc.) | |
| #' @param report_metadata \code{list}; a list representing the properties to create | |
| #' the report with. The names of the list must be one or more of the 3 accepted | |
| #' metadata properties: \code{reportMetadata}, \code{reportTypeMetadata}, | |
| #' \code{reportExtendedMetadata}. | |
| #' @template verbose | |
| #' @return \code{list} representing the newly cloned report with up to 4 properties | |
| #' that describe the report: | |
| #' \describe{ | |
| #' \item{attributes}{Report type along with the URL to retrieve common objects and | |
| #' joined metadata.} | |
| #' \item{reportMetadata}{Unique identifiers for groupings and summaries.} | |
| #' \item{reportTypeMetadata}{Fields in each section of a report type plus filter information for those fields.} | |
| #' \item{reportExtendedMetadata}{Additional information about summaries and groupings.} | |
| #' } | |
| #' @family Report functions | |
| #' @section Salesforce Documentation: | |
| #' \itemize{ | |
| #' \item \href{https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/analytics_api_report_example_post_report.htm}{Documentation} | |
| #' } | |
| #' @examples | |
| #' \dontrun{ | |
| #' # creating a blank report using just the name and type | |
| #' my_new_report <- sf_create_report("Top Accounts Report", "AccountList") | |
| #' | |
| #' # creating a report with additional metadata by grabbing an existing report | |
| #' # and modifying it slightly (only the name in this case) | |
| #' | |
| #' # first, grab all possible reports in your Org | |
| #' all_reports <- sf_query("SELECT Id, Name FROM Report") | |
| #' | |
| #' # second, get the id of the report to copy | |
| #' this_report_id <- all_reports$Id[1] | |
| #' | |
| #' # third, pull down its metadata and update the name | |
| #' report_describe_list <- sf_describe_report(this_report_id) | |
| #' report_describe_list$reportMetadata$name <- "TEST API Report Creation" | |
| #' | |
| #' # fourth, create the report by passing the metadata | |
| #' my_new_report <- sf_create_report(report_metadata=report_describe_list) | |
| #' } | |
| #' @export | |
| sf_create_report <- function(name=NULL, | |
| report_type=NULL, | |
| report_metadata=NULL, | |
| verbose=FALSE){ | |
| if(!is.null(report_metadata)){ | |
| report_metadata <- sf_input_data_validation(report_metadata, | |
| operation='create_report') | |
| } else { | |
| if(is.null(name)){ | |
| stop(paste0("The report name is required. Specify it using the `name` ", | |
| "argument or as part of the `report_metadata` argument"), call.=FALSE) | |
| } | |
| if(is.null(report_type)){ | |
| stop(paste0("The report type is required. Specify it using the `report_type` ", | |
| "argument or as part of the `report_metadata` argument"), call.=FALSE) | |
| } | |
| report_metadata <- list(reportMetadata = | |
| list(name = name, | |
| reportType = | |
| list(type=report_type))) | |
| } | |
| this_url <- make_report_create_url() | |
| httr_response <- rPOST(url = this_url, | |
| body = report_metadata, | |
| encode = "json") | |
| if(verbose){ | |
| make_verbose_httr_message(httr_response$request$method, | |
| httr_response$request$url, | |
| httr_response$request$headers) | |
| } | |
| catch_errors(httr_response) | |
| response_parsed <- content(httr_response, as="parsed", encoding="UTF-8") | |
| return(response_parsed) | |
| } | |
| #' Update a report | |
| #' | |
| #' @description | |
| #' `r lifecycle::badge("experimental")` | |
| #' | |
| #' Save changes to a report by sending a PATCH request to the Report resource. | |
| #' Note that saving a report deletes any running async report jobs because they | |
| #' might be obsolete based on the updates. | |
| #' | |
| #' @template report_id | |
| #' @param report_metadata \code{list}; a list representing the properties to create | |
| #' the report with. The names of the list must be one or more of the 3 accepted | |
| #' metadata properties: \code{reportMetadata}, \code{reportTypeMetadata}, | |
| #' \code{reportExtendedMetadata}. | |
| #' @template verbose | |
| #' @return \code{list} representing the newly cloned report with up to 4 properties | |
| #' that describe the report: | |
| #' \describe{ | |
| #' \item{attributes}{Report type along with the URL to retrieve common objects and | |
| #' joined metadata.} | |
| #' \item{reportMetadata}{Unique identifiers for groupings and summaries.} | |
| #' \item{reportTypeMetadata}{Fields in each section of a report type plus filter information for those fields.} | |
| #' \item{reportExtendedMetadata}{Additional information about summaries and groupings.} | |
| #' } | |
| #' @family Report functions | |
| #' @section Salesforce Documentation: | |
| #' \itemize{ | |
| #' \item \href{https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_save_report.htm#example_save_report}{Example} | |
| #' } | |
| #' @examples | |
| #' \dontrun{ | |
| #' # first, grab all possible reports in your Org | |
| #' all_reports <- sf_query("SELECT Id, Name FROM Report") | |
| #' | |
| #' # second, get the id of the report to update | |
| #' this_report_id <- all_reports$Id[1] | |
| #' | |
| #' my_updated_report <- sf_update_report(this_report_id, | |
| #' report_metadata = | |
| #' list(reportMetadata = | |
| #' list(name = "Updated Report Name!"))) | |
| #' | |
| #' # alternatively, pull down its metadata and update the name | |
| #' report_details <- sf_describe_report(this_report_id) | |
| #' report_details$reportMetadata$name <- paste0(report_details$reportMetadata$name, | |
| #' " - UPDATED") | |
| #' | |
| #' # fourth, update the report by passing the metadata | |
| #' my_updated_report <- sf_update_report(this_report_id, | |
| #' report_metadata = report_details) | |
| #' } | |
| #' @export | |
| sf_update_report <- function(report_id, report_metadata, verbose=FALSE){ | |
| report_metadata <- sf_input_data_validation(report_metadata, | |
| operation='create_report') | |
| this_url <- make_report_url(report_id) | |
| httr_response <- rPATCH(url = this_url, | |
| body = report_metadata, | |
| encode = "json") | |
| if(verbose){ | |
| make_verbose_httr_message(httr_response$request$method, | |
| httr_response$request$url, | |
| httr_response$request$headers) | |
| } | |
| catch_errors(httr_response) | |
| response_parsed <- content(httr_response, as="parsed", encoding="UTF-8") | |
| return(response_parsed) | |
| } | |
| #' Delete a report | |
| #' | |
| #' @description | |
| #' `r lifecycle::badge("experimental")` | |
| #' | |
| #' Delete a report by sending a DELETE request to the Report resource. Deleted | |
| #' reports are moved to the Recycle Bin. | |
| #' | |
| #' @template report_id | |
| #' @template verbose | |
| #' @return \code{logical} indicating whether the report was deleted. This function | |
| #' will return \code{TRUE} if successful in deleting the report. | |
| #' @family Report functions | |
| #' @section Salesforce Documentation: | |
| #' \itemize{ | |
| #' \item \href{https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_delete_report.htm#example_delete_report}{Documentation} | |
| #' } | |
| #' @examples | |
| #' \dontrun{ | |
| #' # first, grab all possible reports in your Org | |
| #' all_reports <- sf_query("SELECT Id, Name FROM Report") | |
| #' | |
| #' # second, get the id of the report to delete | |
| #' this_report_id <- all_reports$Id[1] | |
| #' | |
| #' # third, delete that report using its Id | |
| #' success <- sf_delete_report(this_report_id) | |
| #' } | |
| #' @export | |
| sf_delete_report <- function(report_id, verbose=FALSE){ | |
| this_url <- make_report_url(report_id) | |
| httr_response <- rDELETE(url = this_url) | |
| if(verbose){ | |
| make_verbose_httr_message(httr_response$request$method, | |
| httr_response$request$url, | |
| httr_response$request$headers) | |
| } | |
| catch_errors(httr_response) | |
| response_parsed <- content(httr_response, as="parsed", encoding="UTF-8") | |
| if(is.null(response_parsed) & status_code(httr_response) == 204){ | |
| response_parsed <- TRUE | |
| } | |
| return(invisible(response_parsed)) | |
| } | |
| #' Execute a report | |
| #' | |
| #' @description | |
| #' `r lifecycle::badge("experimental")` | |
| #' | |
| #' Get summary data with or without details by running a report synchronously or | |
| #' asynchronously through the API. When you run a report, the API returns data | |
| #' for the same number of records that are available when the report is run in | |
| #' the Salesforce user interface. Include the \code{filters} argument in your | |
| #' request to get specific results on the fly by passing dynamic filters, | |
| #' groupings, and aggregates in the report metadata. Finally, you may want to | |
| #' use \code{\link{sf_run_report}}. | |
| #' | |
| #' @details Run a report synchronously if you expect it to finish running quickly. | |
| #' Otherwise, we recommend that you run reports through the API asynchronously | |
| #' for these reasons: | |
| #' \itemize{ | |
| #' \item Long running reports have a lower risk of reaching the timeout limit | |
| #' when run asynchronously. | |
| #' \item The 2-minute overall Salesforce API timeout limit doesn’t apply to | |
| #' asynchronous runs. | |
| #' \item The Salesforce Reports and Dashboards REST API can handle a higher | |
| #' number of asynchronous run requests at a time. | |
| #' \item Since the results of an asynchronously run report are stored for a | |
| #' 24-hr rolling period, they’re available for recurring access. | |
| #' } | |
| #' | |
| #' Before you filter a report, it helpful to check the following properties in the metadata | |
| #' that tell you if a field can be filtered, the values and criteria you can filter | |
| #' by, and filters that already exist in the report: | |
| #' \itemize{ | |
| #' \item filterable | |
| #' \item filterValues | |
| #' \item dataTypeFilterOperatorMap | |
| #' \item reportFilters | |
| #' } | |
| #' | |
| #' @importFrom dplyr mutate across select any_of everything | |
| #' @importFrom readr parse_datetime type_convert cols col_guess | |
| #' @importFrom tibble as_tibble_row | |
| #' @importFrom httr content | |
| #' @template report_id | |
| #' @template async | |
| #' @template include_details | |
| #' @template labels | |
| #' @template guess_types | |
| #' @template bind_using_character_cols | |
| #' @template as_tbl | |
| #' @template report_metadata | |
| #' @template verbose | |
| #' @return \code{tbl_df} by default, but a \code{list} when \code{as_tbl=FALSE}, | |
| #' which means that the content from the API is converted from JSON to a list | |
| #' with no other post-processing. | |
| #' @family Report functions | |
| #' @section Salesforce Documentation: | |
| #' \itemize{ | |
| #' \item \href{https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_getreportrundata.htm}{Sync Documentation} | |
| #' \item \href{https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_get_reportdata.htm#example_sync_reportexecute}{Sync Example} | |
| #' \item \href{https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_instances_summaryasync.htm}{Async Documentation} | |
| #' \item \href{https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_get_reportdata.htm#example_report_async_instances}{Async Example} | |
| #' \item \href{https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_filter_reportdata.htm#example_requestbody_execute_resource}{Filtering Results} | |
| #' } | |
| #' @examples | |
| #' \dontrun{ | |
| #' # first, get the Id of a report in your Org | |
| #' all_reports <- sf_query("SELECT Id, Name FROM Report") | |
| #' this_report_id <- all_reports$Id[1] | |
| #' | |
| #' # then execute a synchronous report that will wait for the results | |
| #' results <- sf_execute_report(this_report_id) | |
| #' | |
| #' # alternatively, you can execute an async report and then grab its results when done | |
| #' # - The benefit of an async report is that the results will be stored for up to | |
| #' # 24 hours for faster recall, if needed | |
| #' results <- sf_execute_report(this_report_id, async=TRUE) | |
| #' | |
| #' # check if completed and proceed if the status is "Success" | |
| #' instance_list <- sf_list_report_instances(report_id) | |
| #' instance_status <- instance_list[[which(instance_list$id == results$id), "status"]] | |
| #' if(instance_status == "Success"){ | |
| #' results <- sf_get_report_instance_results(report_id, results$id) | |
| #' } | |
| #' | |
| #' # Note: For more complex execution use the report_metadata argument. | |
| #' # This can be done by building the list from scratch based on Salesforce | |
| #' # documentation (not recommended) or pulling down the existing reportMetadata | |
| #' # property of the report and modifying the list slightly (recommended). | |
| #' # In addition, for relatively simple changes, you can leverage the convenience | |
| #' # function sf_report_wrapper() which makes it easier to retrieve report results | |
| #' report_details <- sf_describe_report(this_report_id) | |
| #' report_metadata <- list(reportMetadata = report_details$reportMetadata) | |
| #' report_metadata$reportMetadata$showGrandTotal <- FALSE | |
| #' report_metadata$reportMetadata$showSubtotals <- FALSE | |
| #' fields <- sf_execute_report(this_report_id, | |
| #' report_metadata = report_metadata) | |
| #' } | |
| #' @export | |
| sf_execute_report <- function(report_id, | |
| async = FALSE, | |
| include_details = TRUE, | |
| labels = TRUE, | |
| guess_types = TRUE, | |
| bind_using_character_cols = FALSE, | |
| as_tbl = TRUE, | |
| report_metadata = NULL, | |
| verbose = FALSE){ | |
| if(!is.null(report_metadata)){ | |
| report_metadata <- sf_input_data_validation(report_metadata, | |
| operation = "filter_report") | |
| } | |
| this_url <- make_report_execute_url(report_id, async, include_details) | |
| if(!is.null(report_metadata)){ | |
| httr_response <- rPOST(url = this_url, | |
| body = report_metadata, | |
| encode = "json") | |
| } else { | |
| if(async){ | |
| httr_response <- rPOST(url = this_url) | |
| } else { | |
| httr_response <- rGET(url = this_url) | |
| } | |
| } | |
| if(verbose){ | |
| make_verbose_httr_message(httr_response$request$method, | |
| httr_response$request$url, | |
| httr_response$request$headers, | |
| report_metadata) | |
| } | |
| catch_errors(httr_response) | |
| response_parsed <- content(httr_response, as="parsed", encoding="UTF-8") | |
| # if as_tbl = FALSE, then return the parsed list, else reformat into a tbl_df | |
| if(as_tbl){ | |
| if(async){ | |
| response_parsed <- response_parsed %>% | |
| set_null_elements_to_na_recursively() %>% | |
| as_tibble_row() %>% | |
| mutate(across(any_of(c("completionDate", "requestDate")), | |
| ~parse_datetime(as.character(.x)))) %>% | |
| type_convert(col_types = cols(.default = col_guess())) %>% | |
| select(any_of(c("id", "ownerId", "status", | |
| "requestDate", "completionDate", | |
| "hasDetailRows", "queryable", "url")), | |
| everything()) | |
| } else { | |
| # parse the same way you would the report instance results | |
| response_parsed <- response_parsed %>% | |
| parse_report_detail_rows( | |
| labels = labels, | |
| guess_types = guess_types, | |
| bind_using_character_cols = bind_using_character_cols | |
| ) | |
| } | |
| } | |
| return(response_parsed) | |
| } | |
| #' List report instances | |
| #' | |
| #' @description | |
| #' `r lifecycle::badge("experimental")` | |
| #' | |
| #' Returns a list of instances for a report that you requested to be run asynchronously. | |
| #' Each item in the list is treated as a separate instance of the report run with | |
| #' metadata in that snapshot of time. | |
| #' | |
| #' @importFrom purrr map_df | |
| #' @importFrom dplyr mutate across select any_of everything | |
| #' @importFrom readr parse_datetime type_convert cols col_guess | |
| #' @template report_id | |
| #' @template as_tbl | |
| #' @template verbose | |
| #' @return \code{tbl_df} by default, or a \code{list} depending on the value of | |
| #' argument \code{as_tbl} | |
| #' @family Report Instance functions | |
| #' @section Salesforce Documentation: | |
| #' \itemize{ | |
| #' \item \href{https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_instances_resource.htm}{Documentation} | |
| #' \item \href{https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_list_asyncreportruns.htm#example_async_fetchresults_instances}{Example} | |
| #' } | |
| #' @examples | |
| #' \dontrun{ | |
| #' # first, get the Id of a report in your Org | |
| #' all_reports <- sf_query("SELECT Id, Name FROM Report") | |
| #' this_report_id <- all_reports$Id[1] | |
| #' | |
| #' # second, execute an async report | |
| #' results <- sf_execute_report(this_report_id, async=TRUE) | |
| #' | |
| #' # third, pull a list of async requests ("instances") usually meant for checking | |
| #' # if a recently requested report has succeeded and the results can be retrieved | |
| #' instance_list <- sf_list_report_instances(this_report_id) | |
| #' instance_status <- instance_list[[which(instance_list$id == results$id), "status"]] | |
| #' } | |
| #' @export | |
| sf_list_report_instances <- function(report_id, as_tbl=TRUE, verbose=FALSE){ | |
| this_url <- make_report_instances_list_url(report_id) | |
| resultset <- sf_rest_list(url=this_url, as_tbl=FALSE, verbose=verbose) | |
| if(as_tbl){ | |
| resultset <- resultset %>% | |
| set_null_elements_to_na_recursively() %>% | |
| map_df(flatten_tbl_df) %>% | |
| mutate(across(any_of(c("completionDate", "requestDate")), | |
| ~parse_datetime(as.character(.x)))) %>% | |
| type_convert(col_types = cols(.default = col_guess())) %>% | |
| select(any_of(c("id", "ownerId", "status", | |
| "requestDate", "completionDate", | |
| "hasDetailRows", "queryable", "url")), | |
| everything()) | |
| } | |
| return(resultset) | |
| } | |
| #' Delete a report instance | |
| #' | |
| #' @description | |
| #' `r lifecycle::badge("experimental")` | |
| #' | |
| #' If the given report instance has a status of \code{Success} or \code{Error}, | |
| #' delete the report instance. | |
| #' | |
| #' @template report_id | |
| #' @template report_instance_id | |
| #' @template verbose | |
| #' @return \code{logical} indicating whether the report instance was deleted. This function | |
| #' will return \code{TRUE} if successful in deleting the report instance. | |
| #' @family Report Instance functions | |
| #' @section Salesforce Documentation: | |
| #' \itemize{ | |
| #' \item \href{https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_instance_resource_results.htm}{Documentation} | |
| #' } | |
| #' @examples | |
| #' \dontrun{ | |
| #' # first, get the Id of a report in your Org | |
| #' all_reports <- sf_query("SELECT Id, Name FROM Report") | |
| #' this_report_id <- all_reports$Id[1] | |
| #' | |
| #' # second, ensure that report has been executed at least once asynchronously | |
| #' results <- sf_execute_report(this_report_id, async=TRUE) | |
| #' | |
| #' # check if that report has succeeded, if so (or if it errored), then delete | |
| #' instance_list <- sf_list_report_instances(this_report_id) | |
| #' instance_status <- instance_list[[which(instance_list$id == results$id), "status"]] | |
| #' } | |
| #' @export | |
| sf_delete_report_instance <- function(report_id, | |
| report_instance_id, | |
| verbose=FALSE){ | |
| this_url <- make_report_instance_url(report_id, report_instance_id) | |
| httr_response <- rDELETE(url = this_url) | |
| if(verbose){ | |
| make_verbose_httr_message(httr_response$request$method, | |
| httr_response$request$url, | |
| httr_response$request$headers) | |
| } | |
| catch_errors(httr_response) | |
| response_parsed <- content(httr_response, as="parsed", encoding="UTF-8") | |
| if(is.null(response_parsed) & status_code(httr_response) == 204){ | |
| response_parsed <- TRUE | |
| } | |
| return(invisible(response_parsed)) | |
| } | |
| #' Get report instance results | |
| #' | |
| #' @description | |
| #' `r lifecycle::badge("experimental")` | |
| #' | |
| #' Retrieves results for an instance of a report run asynchronously with or without | |
| #' filters. Depending on your asynchronous report run request, data can be at the | |
| #' summary level or include details. | |
| #' | |
| #' @importFrom purrr map_df pluck set_names map_chr | |
| #' @template report_id | |
| #' @template report_instance_id | |
| #' @template labels | |
| #' @template guess_types | |
| #' @template bind_using_character_cols | |
| #' @template fact_map_key | |
| #' @template verbose | |
| #' @return \code{tbl_df}; the detail report data. More specifically, the detailed | |
| #' data from the "T!T" entry in the fact map. | |
| #' @family Report Instance functions | |
| #' @section Salesforce Documentation: | |
| #' \itemize{ | |
| #' \item \href{https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_instance_resource_results.htm}{Documentation} | |
| #' \item \href{https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_get_reportdata.htm#example_instance_reportresults}{Example} | |
| #' \item \href{https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_factmap_example.htm}{Factmap Documentation} | |
| #' } | |
| #' @examples | |
| #' \dontrun{ | |
| #' # execute a report asynchronously in your Org | |
| #' all_reports <- sf_query("SELECT Id, Name FROM Report") | |
| #' this_report_id <- all_reports$Id[1] | |
| #' results <- sf_execute_report(this_report_id, async=TRUE) | |
| #' | |
| #' # check if that report has succeeded, ... | |
| #' instance_list <- sf_list_report_instances(this_report_id) | |
| #' instance_status <- instance_list[[which(instance_list$id == results$id), "status"]] | |
| #' | |
| #' # ... if so, then grab the results | |
| #' if(instance_status == "Success"){ | |
| #' report_data <- sf_get_report_instance_results(report_id = this_report_id, | |
| #' report_instance_id = results$id) | |
| #' } | |
| #' } | |
| #' @export | |
| sf_get_report_instance_results <- function(report_id, | |
| report_instance_id, | |
| labels = TRUE, | |
| guess_types = TRUE, | |
| bind_using_character_cols = FALSE, | |
| fact_map_key = "T!T", | |
| verbose = FALSE){ | |
| 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 | |
| ) | |
| return(resultset) | |
| } | |
| #' Get a report's data in tabular format | |
| #' | |
| #' @description | |
| #' `r lifecycle::badge("experimental")` | |
| #' | |
| #' This function is a convenience wrapper for retrieving the data from a report. | |
| #' By default, it executes an asynchronous report and waits for the detailed data | |
| #' summarized in a tabular format, before pulling them down and returning as a | |
| #' \code{tbl_df}. | |
| #' | |
| #' @details This function is essentially a wrapper around \code{\link{sf_execute_report}}. | |
| #' Please review or use that function and/or \code{\link{sf_query_report}} if you | |
| #' want to have more control over how the report is run and what format should | |
| #' be returned. In this case we've forced the \code{reportFormat="TABULAR"} | |
| #' without total rows and given options to filter, and select the Top N as | |
| #' function arguments rather than forcing the user to create an entire list of | |
| #' \code{reportMetadata}. | |
| #' | |
| #' @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 | |
| #' 3) \code{value}. You can find out how certain field types can be filtered by | |
| #' reviewing the results of \code{\link{sf_list_report_filter_operators}}. | |
| #' @param report_boolean_logic \code{character}; a string of boolean logic to parse | |
| #' custom field filters if more than one is specified. For example, if three filters | |
| #' are specified, then they can be combined using the logic \code{"(1 OR 2) AND 3"}. | |
| #' @param sort_by \code{character}; the name of the column(s) used to sort the results. | |
| #' @param decreasing \code{logical}; the indicator(s) of whether each column in the | |
| #' \code{sort_by} argument should be ordered by increasing or decreasing values. If | |
| #' the length is shorter than the length of the sort_by argument then the elements | |
| #' will be recycled. | |
| #' @param top_n \code{integer}; an integer which sets a row limit filter to a report. | |
| #' The results will be ordered as they appear in the report unless specified differently | |
| #' via the \code{sort_by} and \code{decreasing} arguments. Note, it is sometimes | |
| #' helpful to specify the \code{top_n} argument if a report contains many rows, but | |
| #' you are only interested in a subset of them. Alternatively, you can limit the count | |
| #' of returned rows via the \code{report_filters} argument. | |
| #' @param decreasing \code{logical}; a indicator of whether the results should be | |
| #' ordered by increasing or decreasing values in \code{sort_by} column when selecting the | |
| #' top N records. Note, this argument will be ignored if not specifying Top N. You can | |
| #' sort the records using \code{\link[dplyr]{arrange}} after the results are returned. | |
| #' @template async | |
| #' @template interval_seconds | |
| #' @template max_attempts | |
| #' @param wait_for_results \code{logical}; indicating whether to wait for the | |
| #' 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 verbose | |
| #' @return \code{tbl_df} | |
| #' @family Report functions | |
| #' @section Salesforce Documentation: | |
| #' \itemize{ | |
| #' \item \href{https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_getreportrundata.htm}{Sync Documentation} | |
| #' \item \href{https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_get_reportdata.htm#example_sync_reportexecute}{Sync Example} | |
| #' \item \href{https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_instances_summaryasync.htm}{Async Documentation} | |
| #' \item \href{https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_get_reportdata.htm#example_report_async_instances}{Async Example} | |
| #' \item \href{https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_filter_reportdata.htm#example_requestbody_execute_resource}{Filtering Results} | |
| #' } | |
| #' @examples | |
| #' \dontrun{ | |
| #' # find a report in your org and run it | |
| #' all_reports <- sf_query("SELECT Id, Name FROM Report") | |
| #' this_report_id <- all_reports$Id[1] | |
| #' results <- sf_run_report(this_report_id) | |
| #' | |
| #' # apply your own filters to that same report | |
| #' # set up some filters, if needed | |
| #' # filter records that was created before this month | |
| #' filter1 <- list(column = "CREATED_DATE", | |
| #' operator = "lessThan", | |
| #' value = "THIS_MONTH") | |
| #' | |
| #' # filter records where the account billing address city is not empty | |
| #' filter2 <- list(column = "ACCOUNT.ADDRESS1_CITY", | |
| #' operator = "notEqual", | |
| #' value = "") | |
| #' | |
| #' # combine filter1 and filter2 using 'AND' so that records must meet both filters | |
| #' results_using_AND <- sf_run_report(my_report_id, | |
| #' report_boolean_logic = "1 AND 2", | |
| #' report_filters = list(filter1, filter2)) | |
| #' | |
| #' # combine filter1 and filter2 using 'OR' which means that records must meet one | |
| #' # of the filters but also throw in a row limit based on a specific sort order | |
| #' results_using_OR <- sf_run_report(my_report_id, | |
| #' report_boolean_logic = "1 OR 2", | |
| #' report_filters = list(filter1, filter2), | |
| #' sort_by = "Contact.test_number__c", | |
| #' decreasing = TRUE, | |
| #' top_n = 5) | |
| #' } | |
| #' @export | |
| sf_run_report <- function(report_id, | |
| report_filters = NULL, | |
| report_boolean_logic = NULL, | |
| sort_by = character(0), | |
| decreasing = FALSE, | |
| top_n = NULL, | |
| async = TRUE, | |
| interval_seconds = 3, | |
| max_attempts = 200, | |
| wait_for_results = TRUE, | |
| verbose = FALSE){ | |
| # 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) | |
| if(!is.null(report_filters)){ | |
| stopifnot(is.list(report_filters)) | |
| for(i in 1:length(report_filters)){ | |
| report_filters[[i]] <- metadata_type_validator(obj_type = "ReportFilterItem", | |
| obj_data = report_filters[[i]])[[1]] | |
| } | |
| if(is.null(report_boolean_logic)){ | |
| if(length(report_filters) > 1){ | |
| report_boolean_logic <- paste((1:length(report_filters)), collapse=" AND ") | |
| message(sprintf(paste0("The argument `report_boolean_logic` was left NULL. ", | |
| "Assuming the report filters should be combined using 'AND' ", | |
| "like so: %s"), report_boolean_logic)) | |
| } else { | |
| report_boolean_logic <- NA | |
| } | |
| } else { | |
| stopifnot(is.character(report_boolean_logic)) | |
| } | |
| } else { | |
| # value must be null when filter logic is not specified | |
| report_boolean_logic <- NA | |
| } | |
| request_body$reportMetadata$reportBooleanFilter <- report_boolean_logic | |
| request_body$reportMetadata$reportFilters <- report_filters | |
| if(length(sort_by) > 0 & !(all(is.na(sort_by)))){ | |
| if(length(sort_by) > 1){ | |
| stop(paste0("Currently, Salesforce will only allow a report to be sorted ", | |
| "by, at most, one column."), call. = FALSE) | |
| } else { | |
| if(length(decreasing) < length(sort_by)){ | |
| decreasing <- rep_len(decreasing, length.out = length(sort_by)) | |
| } | |
| sort_list_spec <- list() | |
| for(i in 1:length(sort_by)){ | |
| sort_list_spec[[i]] <- list(sortColumn = sort_by[i], | |
| sortOrder = if(decreasing[i]) "Desc" else "Asc") | |
| } | |
| request_body$reportMetadata$sortBy <- sort_list_spec | |
| } | |
| } else { | |
| # if there is no sortBy, then set it to NA, if there is, then leave it alone | |
| # beause it is required when Top N is specified and the user might just want | |
| # to use the existing sort order in the report | |
| if(is.null(request_body$reportMetadata$sortBy) || | |
| is.na(request_body$reportMetadata$sortBy) || | |
| length(request_body$reportMetadata$sortBy) == 0){ | |
| request_body$reportMetadata$sortBy <- NA | |
| } | |
| } | |
| if(!is.null(top_n)){ | |
| if(is.na(request_body$reportMetadata$sortBy)){ | |
| stop(paste0("A report must be sorted by one column when requesting a ", | |
| "Top N number of rows."), call. = FALSE) | |
| } else if(length(request_body$reportMetadata$sortBy) > 1){ | |
| stop(paste0("A report can only be sorted by one column when requesting a ", | |
| "Top N number of rows."), call. = FALSE) | |
| } else{ | |
| # the direction is always 'Asc' because Salesforce doesn't accept 'Desc'. It | |
| # relies on the 'sortOrder' element within the 'sortBy' element | |
| request_body$reportMetadata$topRows <- list(rowLimit = top_n, direction = "Asc") | |
| } | |
| } | |
| results <- sf_execute_report(report_id, | |
| async = async, | |
| report_metadata = request_body, | |
| verbose = verbose) | |
| # request the report results (still wait if async is specified) | |
| if(async){ | |
| if(wait_for_results){ | |
| status_complete <- FALSE | |
| z <- 1 | |
| Sys.sleep(interval_seconds) | |
| while (z < max_attempts & !status_complete){ | |
| if (verbose){ | |
| if(z %% 5 == 0){ | |
| message(paste0("Attempt to retrieve records #", z)) | |
| } | |
| } | |
| Sys.sleep(interval_seconds) | |
| instances_list <- sf_list_report_instances(report_id, verbose = verbose) | |
| instance_status <- instances_list[[which(instances_list$id == results$id), "status"]] | |
| if(instance_status == "Error"){ | |
| stop(sprintf("Report run failed (Report Id: %s; Instance Id: %s).", | |
| report_id, results$id), | |
| call.=FALSE) | |
| } else { | |
| if(instance_status == "Success"){ | |
| status_complete <- TRUE | |
| } else { | |
| # continue checking the status until success or max attempts | |
| z <- z + 1 | |
| } | |
| } | |
| } | |
| results <- sf_get_report_instance_results(report_id, | |
| results$id, | |
| verbose = verbose) | |
| } | |
| } | |
| # if not aysnc and waiting for results, then sf_execute_report() will return | |
| # the parsed dataset (if sync) or request details if async to check on the results | |
| # without having the wrapper executing the wait. This is so users can leverage | |
| # the simpler interface (i.e. providing function arguments) instead of researching | |
| # the Salesforce documentation and building the reportMetadata property from scratch | |
| return(results) | |
| } | |
| #' Get Report Data without Saving Changes to or Creating a Report | |
| #' | |
| #' @description | |
| #' `r lifecycle::badge("experimental")` | |
| #' | |
| #' Run a report without creating a new report or changing the existing one by making | |
| #' a POST request to the query resource. This allows you to get report data | |
| #' without filling up your Org with unnecessary reports. | |
| #' | |
| #' @details Note that you can query a report's data simply by providing its \code{Id}. | |
| #' However, the data will only be the detailed data from the tabular format | |
| #' with no totals or other metadata. If you would like more control, for example, | |
| #' filtering the results or grouping them in specific ways, then you will need | |
| #' to specify a list to the \code{report_metadata} argument. The \code{report_metadata} | |
| #' argument requires specific knowledge on the structure the \code{reportMetadata} | |
| #' property of a report. For more information, please review the Salesforce documentation | |
| #' in detail \href{https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_getbasic_reportmetadata.htm#analyticsapi_basicmetadata}{HERE}. | |
| #' Additional references are provided in the \code{"See Also"} section. | |
| #' | |
| #' @template report_id | |
| #' @template report_metadata | |
| #' @template verbose | |
| #' @return \code{tbl_df} | |
| #' @family Report functions | |
| #' @section Salesforce Documentation: | |
| #' \itemize{ | |
| #' \item \href{https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_report_query.htm}{Documentation} | |
| #' \item \href{https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_report_query_example.htm#sforce_analytics_rest_api_report_query_example}{Example} | |
| #' } | |
| #' @export | |
| sf_query_report <- function(report_id, | |
| report_metadata = NULL, | |
| verbose = FALSE){ | |
| .NotYetImplemented() | |
| # if(!is.null(report_metadata)){ | |
| # report_metadata <- sf_input_data_validation(report_metadata, | |
| # operation = "filter_report") | |
| # } else { | |
| # # copy existing report metadata and then set options to something simpler, | |
| # # meaning no filters, no aggregates, no totals or subtotals, and at the | |
| # # detail level in tablular format | |
| # report_metadata <- simplify_report_metadata(report_id, verbose = verbose) | |
| # } | |
| # | |
| # this_url <- make_report_query_url() | |
| # httr_response <- rPOST(url = this_url, | |
| # body = report_metadata, | |
| # encode = "json") | |
| # if(verbose){ | |
| # make_verbose_httr_message(httr_response$request$method, | |
| # httr_response$request$url, | |
| # httr_response$request$headers, | |
| # report_metadata) | |
| # } | |
| # catch_errors(httr_response) | |
| # response_parsed <- content(httr_response, as="parsed", encoding="UTF-8") | |
| # resultset <- parse_report_detail_rows(response_parsed) | |
| # return(resultset) | |
| } |