Skip to main content

busbar_sf_rest/client/
collections.rs

1use serde::{de::DeserializeOwned, Serialize};
2use tracing::instrument;
3
4use busbar_sf_client::security::{soql, url as url_security};
5
6use crate::collections::{CollectionRequest, CollectionResult};
7use crate::error::{Error, ErrorKind, Result};
8
9impl super::SalesforceRestClient {
10    /// Create multiple records in a single request (up to 200).
11    #[instrument(skip(self, records))]
12    pub async fn create_multiple<T: Serialize>(
13        &self,
14        sobject: &str,
15        records: &[T],
16        all_or_none: bool,
17    ) -> Result<Vec<CollectionResult>> {
18        if !soql::is_safe_sobject_name(sobject) {
19            return Err(Error::new(ErrorKind::Salesforce {
20                error_code: "INVALID_SOBJECT".to_string(),
21                message: "Invalid SObject name".to_string(),
22            }));
23        }
24        let request = CollectionRequest {
25            all_or_none,
26            records: records
27                .iter()
28                .map(|r| {
29                    let mut value = serde_json::to_value(r).unwrap_or(serde_json::Value::Null);
30                    if let serde_json::Value::Object(ref mut map) = value {
31                        map.insert(
32                            "attributes".to_string(),
33                            serde_json::json!({"type": sobject}),
34                        );
35                    }
36                    value
37                })
38                .collect(),
39        };
40        self.client
41            .rest_post("composite/sobjects", &request)
42            .await
43            .map_err(Into::into)
44    }
45
46    /// Update multiple records in a single request (up to 200).
47    #[instrument(skip(self, records))]
48    pub async fn update_multiple<T: Serialize>(
49        &self,
50        sobject: &str,
51        records: &[(String, T)], // (id, record)
52        all_or_none: bool,
53    ) -> Result<Vec<CollectionResult>> {
54        if !soql::is_safe_sobject_name(sobject) {
55            return Err(Error::new(ErrorKind::Salesforce {
56                error_code: "INVALID_SOBJECT".to_string(),
57                message: "Invalid SObject name".to_string(),
58            }));
59        }
60        // Validate all IDs
61        for (id, _) in records {
62            if !url_security::is_valid_salesforce_id(id) {
63                return Err(Error::new(ErrorKind::Salesforce {
64                    error_code: "INVALID_ID".to_string(),
65                    message: "Invalid Salesforce ID format".to_string(),
66                }));
67            }
68        }
69        let request = CollectionRequest {
70            all_or_none,
71            records: records
72                .iter()
73                .map(|(id, r)| {
74                    let mut value = serde_json::to_value(r).unwrap_or(serde_json::Value::Null);
75                    if let serde_json::Value::Object(ref mut map) = value {
76                        map.insert(
77                            "attributes".to_string(),
78                            serde_json::json!({"type": sobject}),
79                        );
80                        map.insert("Id".to_string(), serde_json::json!(id));
81                    }
82                    value
83                })
84                .collect(),
85        };
86
87        let url = self.client.rest_url("composite/sobjects");
88        let request_builder = self.client.patch(&url).json(&request)?;
89        let response = self.client.execute(request_builder).await?;
90        response.json().await.map_err(Into::into)
91    }
92
93    /// Delete multiple records in a single request (up to 200).
94    #[instrument(skip(self))]
95    pub async fn delete_multiple(
96        &self,
97        ids: &[&str],
98        all_or_none: bool,
99    ) -> Result<Vec<CollectionResult>> {
100        // Validate all IDs before proceeding
101        for id in ids {
102            if !url_security::is_valid_salesforce_id(id) {
103                return Err(Error::new(ErrorKind::Salesforce {
104                    error_code: "INVALID_ID".to_string(),
105                    message: "Invalid Salesforce ID format".to_string(),
106                }));
107            }
108        }
109        let ids_param = ids.join(",");
110        let url = format!(
111            "{}/services/data/v{}/composite/sobjects?ids={}&allOrNone={}",
112            self.client.instance_url(),
113            self.client.api_version(),
114            ids_param,
115            all_or_none
116        );
117        let request = self.client.delete(&url);
118        let response = self.client.execute(request).await?;
119        response.json().await.map_err(Into::into)
120    }
121
122    /// Get multiple records by ID in a single request (up to 2000).
123    #[instrument(skip(self))]
124    pub async fn get_multiple<T: DeserializeOwned>(
125        &self,
126        sobject: &str,
127        ids: &[&str],
128        fields: &[&str],
129    ) -> Result<Vec<T>> {
130        if !soql::is_safe_sobject_name(sobject) {
131            return Err(Error::new(ErrorKind::Salesforce {
132                error_code: "INVALID_SOBJECT".to_string(),
133                message: "Invalid SObject name".to_string(),
134            }));
135        }
136        // Validate all IDs
137        for id in ids {
138            if !url_security::is_valid_salesforce_id(id) {
139                return Err(Error::new(ErrorKind::Salesforce {
140                    error_code: "INVALID_ID".to_string(),
141                    message: "Invalid Salesforce ID format".to_string(),
142                }));
143            }
144        }
145        // Validate and filter field names
146        let safe_fields: Vec<&str> = soql::filter_safe_fields(fields.iter().copied()).collect();
147        if safe_fields.is_empty() {
148            return Err(Error::new(ErrorKind::Salesforce {
149                error_code: "INVALID_FIELDS".to_string(),
150                message: "No valid field names provided".to_string(),
151            }));
152        }
153        let ids_param = ids.join(",");
154        let fields_param = safe_fields.join(",");
155        let url = format!(
156            "{}/services/data/v{}/composite/sobjects/{}?ids={}&fields={}",
157            self.client.instance_url(),
158            self.client.api_version(),
159            sobject,
160            ids_param,
161            fields_param
162        );
163        // The SObject Collections GET response is a JSON array that may contain
164        // null entries for records that could not be retrieved (deleted, no access, etc.).
165        // Deserialize as Vec<Option<T>> and filter out the nulls.
166        let results: Vec<Option<T>> = self.client.get_json(&url).await.map_err(Error::from)?;
167        Ok(results.into_iter().flatten().collect())
168    }
169}