salesforce_client/
crud.rs

1//! CRUD operations (Create, Read, Update, Delete)
2//!
3//! Provides type-safe methods for manipulating Salesforce records.
4
5use crate::error::{SfError, SfResult};
6use serde::{Deserialize, Serialize};
7use tracing::{debug, info};
8
9/// Response from a successful insert operation
10#[derive(Debug, Deserialize, Clone)]
11pub struct InsertResponse {
12    /// The ID of the newly created record
13    pub id: String,
14
15    /// Whether the operation was successful
16    pub success: bool,
17
18    /// Any errors that occurred
19    #[serde(default)]
20    pub errors: Vec<SalesforceError>,
21}
22
23/// Response from an update or delete operation
24#[derive(Debug, Deserialize, Clone)]
25pub struct UpdateResponse {
26    /// Whether the operation was successful
27    pub success: bool,
28
29    /// Any errors that occurred
30    #[serde(default)]
31    pub errors: Vec<SalesforceError>,
32}
33
34/// Salesforce error detail
35#[derive(Debug, Deserialize, Clone)]
36pub struct SalesforceError {
37    /// Error status code
38    #[serde(rename = "statusCode")]
39    pub status_code: String,
40
41    /// Error message
42    pub message: String,
43
44    /// Fields that caused the error
45    #[serde(default)]
46    pub fields: Vec<String>,
47}
48
49/// Batch response for multiple operations
50#[derive(Debug, Deserialize)]
51pub struct BatchResponse {
52    /// Whether the batch operation succeeded
53    #[serde(rename = "hasErrors")]
54    pub has_errors: bool,
55
56    /// Results for each record
57    pub results: Vec<BatchResult>,
58}
59
60/// Result for a single record in a batch operation
61#[derive(Debug, Deserialize)]
62pub struct BatchResult {
63    /// Status code
64    #[serde(rename = "statusCode")]
65    pub status_code: u16,
66
67    /// Result details (ID for insert, empty for update/delete)
68    pub result: Option<serde_json::Value>,
69}
70
71/// Builder for upsert operations
72#[derive(Debug)]
73pub struct UpsertBuilder {
74    /// External ID field name
75    pub external_id_field: String,
76
77    /// External ID value
78    pub external_id_value: String,
79}
80
81impl Clone for UpsertBuilder {
82    fn clone(&self) -> Self {
83        Self {
84            external_id_field: self.external_id_field.clone(),
85            external_id_value: self.external_id_value.clone(),
86        }
87    }
88}
89
90impl UpsertBuilder {
91    /// Create a new upsert builder
92    pub fn new(external_id_field: impl Into<String>, external_id_value: impl Into<String>) -> Self {
93        Self {
94            external_id_field: external_id_field.into(),
95            external_id_value: external_id_value.into(),
96        }
97    }
98}
99
100/// CRUD operations implementation
101pub(crate) struct CrudOperations {
102    http_client: reqwest::Client,
103    base_url: String,
104    access_token: String,
105}
106
107impl CrudOperations {
108    /// Create a new CRUD operations handler
109    pub fn new(http_client: reqwest::Client, base_url: String, access_token: String) -> Self {
110        Self {
111            http_client,
112            base_url,
113            access_token,
114        }
115    }
116
117    /// Insert a new record
118    ///
119    /// # Example
120    /// ```ignore
121    /// #[derive(Serialize)]
122    /// struct NewAccount {
123    ///     #[serde(rename = "Name")]
124    ///     name: String,
125    /// }
126    ///
127    /// let account = NewAccount { name: "Acme Corp".to_string() };
128    /// let response = client.insert("Account", &account).await?;
129    /// println!("Created account with ID: {}", response.id);
130    /// ```
131    pub async fn insert<T: Serialize>(&self, sobject: &str, data: &T) -> SfResult<InsertResponse> {
132        let url = format!("{}/services/data/v57.0/sobjects/{}", self.base_url, sobject);
133
134        debug!("Inserting {} record", sobject);
135
136        let response = self
137            .http_client
138            .post(&url)
139            .header("Authorization", format!("Bearer {}", self.access_token))
140            .header("Content-Type", "application/json")
141            .json(data)
142            .send()
143            .await?;
144
145        let status = response.status();
146        if !status.is_success() {
147            let body = response.text().await?;
148            return Err(SfError::Api {
149                status: status.as_u16(),
150                body,
151            });
152        }
153
154        let insert_response: InsertResponse = response.json().await?;
155
156        if !insert_response.success {
157            let error_msg = insert_response
158                .errors
159                .iter()
160                .map(|e| format!("{}: {}", e.status_code, e.message))
161                .collect::<Vec<_>>()
162                .join(", ");
163            return Err(SfError::Api {
164                status: 400,
165                body: error_msg,
166            });
167        }
168
169        info!(
170            "Successfully inserted {} with ID: {}",
171            sobject, insert_response.id
172        );
173        Ok(insert_response)
174    }
175
176    /// Update an existing record
177    ///
178    /// # Example
179    /// ```ignore
180    /// #[derive(Serialize)]
181    /// struct AccountUpdate {
182    ///     #[serde(rename = "Name")]
183    ///     name: String,
184    /// }
185    ///
186    /// let update = AccountUpdate { name: "New Name".to_string() };
187    /// client.update("Account", "001xx000003DGbX", &update).await?;
188    /// ```
189    pub async fn update<T: Serialize>(&self, sobject: &str, id: &str, data: &T) -> SfResult<()> {
190        let url = format!(
191            "{}/services/data/v57.0/sobjects/{}/{}",
192            self.base_url, sobject, id
193        );
194
195        debug!("Updating {} record {}", sobject, id);
196
197        let response = self
198            .http_client
199            .patch(&url)
200            .header("Authorization", format!("Bearer {}", self.access_token))
201            .header("Content-Type", "application/json")
202            .json(data)
203            .send()
204            .await?;
205
206        let status = response.status();
207        if status == reqwest::StatusCode::NOT_FOUND {
208            return Err(SfError::NotFound {
209                sobject: sobject.to_string(),
210                id: id.to_string(),
211            });
212        }
213
214        if !status.is_success() {
215            let body = response.text().await?;
216            return Err(SfError::Api {
217                status: status.as_u16(),
218                body,
219            });
220        }
221
222        info!("Successfully updated {} {}", sobject, id);
223        Ok(())
224    }
225
226    /// Delete a record
227    pub async fn delete(&self, sobject: &str, id: &str) -> SfResult<()> {
228        let url = format!(
229            "{}/services/data/v57.0/sobjects/{}/{}",
230            self.base_url, sobject, id
231        );
232
233        debug!("Deleting {} record {}", sobject, id);
234
235        let response = self
236            .http_client
237            .delete(&url)
238            .header("Authorization", format!("Bearer {}", self.access_token))
239            .send()
240            .await?;
241
242        let status = response.status();
243        if status == reqwest::StatusCode::NOT_FOUND {
244            return Err(SfError::NotFound {
245                sobject: sobject.to_string(),
246                id: id.to_string(),
247            });
248        }
249
250        if !status.is_success() {
251            let body = response.text().await?;
252            return Err(SfError::Api {
253                status: status.as_u16(),
254                body,
255            });
256        }
257
258        info!("Successfully deleted {} {}", sobject, id);
259        Ok(())
260    }
261
262    /// Upsert a record (insert or update based on external ID)
263    ///
264    /// # Example
265    /// ```ignore
266    /// let upsert = UpsertBuilder::new("ExternalId__c", "ext-12345");
267    /// client.upsert("Account", upsert, &account_data).await?;
268    /// ```
269    pub async fn upsert<T: Serialize>(
270        &self,
271        sobject: &str,
272        builder: UpsertBuilder,
273        data: &T,
274    ) -> SfResult<InsertResponse> {
275        let url = format!(
276            "{}/services/data/v57.0/sobjects/{}/{}/{}",
277            self.base_url, sobject, builder.external_id_field, builder.external_id_value
278        );
279
280        debug!(
281            "Upserting {} record with external ID {}",
282            sobject, builder.external_id_value
283        );
284
285        let response = self
286            .http_client
287            .patch(&url)
288            .header("Authorization", format!("Bearer {}", self.access_token))
289            .header("Content-Type", "application/json")
290            .json(data)
291            .send()
292            .await?;
293
294        let status = response.status();
295        if !status.is_success() {
296            let body = response.text().await?;
297            return Err(SfError::Api {
298                status: status.as_u16(),
299                body,
300            });
301        }
302
303        let upsert_response: InsertResponse = response.json().await?;
304        info!(
305            "Successfully upserted {} with ID: {}",
306            sobject, upsert_response.id
307        );
308
309        Ok(upsert_response)
310    }
311}