Skip to main content

busbar_sf_tooling/client/
composite.rs

1use busbar_sf_client::security::soql;
2use tracing::instrument;
3
4use crate::error::{Error, ErrorKind, Result};
5
6impl super::ToolingClient {
7    /// Execute a Tooling API composite request with multiple subrequests.
8    ///
9    /// The Tooling API composite endpoint allows up to 25 subrequests in a single API call.
10    /// Subrequests can reference results from earlier subrequests using `@{referenceId}`.
11    ///
12    /// Available since API v40.0.
13    ///
14    /// # Example
15    ///
16    /// ```rust,ignore
17    /// use busbar_sf_tooling::{CompositeRequest, CompositeSubrequest};
18    ///
19    /// let request = CompositeRequest {
20    ///     all_or_none: false,
21    ///     collate_subrequests: false,
22    ///     subrequests: vec![
23    ///         CompositeSubrequest {
24    ///             method: "GET".to_string(),
25    ///             url: "/services/data/v62.0/tooling/sobjects/ApexClass/01p...".to_string(),
26    ///             reference_id: "refApexClass".to_string(),
27    ///             body: None,
28    ///         },
29    ///     ],
30    /// };
31    ///
32    /// let response = client.composite(&request).await?;
33    /// ```
34    #[instrument(skip(self, request))]
35    pub async fn composite(
36        &self,
37        request: &busbar_sf_rest::CompositeRequest,
38    ) -> Result<busbar_sf_rest::CompositeResponse> {
39        let url = self.client.tooling_url("composite");
40        self.client
41            .post_json(&url, request)
42            .await
43            .map_err(Into::into)
44    }
45
46    /// Execute a Tooling API composite batch request with multiple independent subrequests.
47    ///
48    /// The composite batch API executes up to 25 subrequests independently.
49    /// Unlike the standard composite API, subrequests cannot reference each other's results.
50    ///
51    /// Available since API v40.0.
52    #[instrument(skip(self, request))]
53    pub async fn composite_batch(
54        &self,
55        request: &busbar_sf_rest::CompositeBatchRequest,
56    ) -> Result<busbar_sf_rest::CompositeBatchResponse> {
57        let url = self.client.tooling_url("composite/batch");
58        self.client
59            .post_json(&url, request)
60            .await
61            .map_err(Into::into)
62    }
63
64    /// Execute a Tooling API composite tree request to create record hierarchies.
65    ///
66    /// Creates parent records with nested child records in a single request.
67    /// Supports up to 200 records total across all levels of the hierarchy.
68    ///
69    /// Available since API v42.0.
70    ///
71    /// # Arguments
72    /// * `sobject` - The parent SObject type (e.g., "ApexClass", "CustomField")
73    /// * `request` - The tree request containing parent records and nested children
74    #[instrument(skip(self, request))]
75    pub async fn composite_tree(
76        &self,
77        sobject: &str,
78        request: &busbar_sf_rest::CompositeTreeRequest,
79    ) -> Result<busbar_sf_rest::CompositeTreeResponse> {
80        if !soql::is_safe_sobject_name(sobject) {
81            return Err(Error::new(ErrorKind::Salesforce {
82                error_code: "INVALID_SOBJECT".to_string(),
83                message: "Invalid SObject name".to_string(),
84            }));
85        }
86        let url = self
87            .client
88            .tooling_url(&format!("composite/tree/{}", sobject));
89        self.client
90            .post_json(&url, request)
91            .await
92            .map_err(Into::into)
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::super::ToolingClient;
99
100    #[test]
101    fn test_composite_url_construction() {
102        let client = ToolingClient::new("https://na1.salesforce.com", "token").unwrap();
103
104        let url = client.client.tooling_url("composite");
105        assert_eq!(
106            url,
107            "https://na1.salesforce.com/services/data/v62.0/tooling/composite"
108        );
109    }
110
111    #[test]
112    fn test_composite_batch_url_construction() {
113        let client = ToolingClient::new("https://na1.salesforce.com", "token").unwrap();
114
115        let url = client.client.tooling_url("composite/batch");
116        assert_eq!(
117            url,
118            "https://na1.salesforce.com/services/data/v62.0/tooling/composite/batch"
119        );
120    }
121
122    #[test]
123    fn test_composite_tree_url_construction() {
124        let client = ToolingClient::new("https://na1.salesforce.com", "token").unwrap();
125
126        let url = client
127            .client
128            .tooling_url(&format!("composite/tree/{}", "ApexClass"));
129        assert_eq!(
130            url,
131            "https://na1.salesforce.com/services/data/v62.0/tooling/composite/tree/ApexClass"
132        );
133    }
134}