1use crate::error::{SfError, SfResult};
6use serde::{Deserialize, Serialize};
7use tracing::{debug, info};
8
9#[derive(Debug, Deserialize, Clone)]
11pub struct InsertResponse {
12 pub id: String,
14
15 pub success: bool,
17
18 #[serde(default)]
20 pub errors: Vec<SalesforceError>,
21}
22
23#[derive(Debug, Deserialize, Clone)]
25pub struct UpdateResponse {
26 pub success: bool,
28
29 #[serde(default)]
31 pub errors: Vec<SalesforceError>,
32}
33
34#[derive(Debug, Deserialize, Clone)]
36pub struct SalesforceError {
37 #[serde(rename = "statusCode")]
39 pub status_code: String,
40
41 pub message: String,
43
44 #[serde(default)]
46 pub fields: Vec<String>,
47}
48
49#[derive(Debug, Deserialize)]
51pub struct BatchResponse {
52 #[serde(rename = "hasErrors")]
54 pub has_errors: bool,
55
56 pub results: Vec<BatchResult>,
58}
59
60#[derive(Debug, Deserialize)]
62pub struct BatchResult {
63 #[serde(rename = "statusCode")]
65 pub status_code: u16,
66
67 pub result: Option<serde_json::Value>,
69}
70
71#[derive(Debug)]
73pub struct UpsertBuilder {
74 pub external_id_field: String,
76
77 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 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
100pub(crate) struct CrudOperations {
102 http_client: reqwest::Client,
103 base_url: String,
104 access_token: String,
105}
106
107impl CrudOperations {
108 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 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 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 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 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}