busbar_sf_tooling/client/
query.rs1use serde::de::DeserializeOwned;
2use tracing::instrument;
3
4use busbar_sf_client::QueryResult;
5
6use crate::error::Result;
7use crate::types::SearchResult;
8
9impl super::ToolingClient {
10 #[instrument(skip(self))]
27 pub async fn query<T: DeserializeOwned>(&self, soql: &str) -> Result<QueryResult<T>> {
28 self.client.tooling_query(soql).await.map_err(Into::into)
29 }
30
31 #[instrument(skip(self))]
38 pub async fn query_all<T: DeserializeOwned + Clone>(&self, soql: &str) -> Result<Vec<T>> {
39 self.client
40 .tooling_query_all(soql)
41 .await
42 .map_err(Into::into)
43 }
44
45 #[instrument(skip(self))]
51 pub async fn query_all_records<T: DeserializeOwned>(
52 &self,
53 soql: &str,
54 ) -> Result<QueryResult<T>> {
55 let encoded = urlencoding::encode(soql);
56 let url = format!(
57 "{}/services/data/v{}/queryAll/?q={}",
58 self.client.instance_url(),
59 self.client.api_version(),
60 encoded
61 );
62 self.client.get_json(&url).await.map_err(Into::into)
63 }
64
65 #[instrument(skip(self))]
67 pub async fn search<T: DeserializeOwned>(&self, sosl: &str) -> Result<SearchResult<T>> {
68 let encoded = urlencoding::encode(sosl);
69 let url = format!(
70 "{}/services/data/v{}/tooling/search/?q={}",
71 self.client.instance_url(),
72 self.client.api_version(),
73 encoded
74 );
75 self.client.get_json(&url).await.map_err(Into::into)
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::super::ToolingClient;
82
83 #[tokio::test]
84 async fn test_query_all_records_wiremock() {
85 use wiremock::matchers::{method, path_regex};
86 use wiremock::{Mock, MockServer, ResponseTemplate};
87
88 let mock_server = MockServer::start().await;
89 let body = serde_json::json!({
90 "totalSize": 2,
91 "done": true,
92 "records": [
93 {"Id": "01p000000000001AAA", "Name": "DeletedClass", "IsDeleted": true},
94 {"Id": "01p000000000002AAA", "Name": "ArchivedClass", "IsDeleted": true}
95 ]
96 });
97
98 Mock::given(method("GET"))
99 .and(path_regex(".*/queryAll/"))
100 .respond_with(ResponseTemplate::new(200).set_body_json(&body))
101 .mount(&mock_server)
102 .await;
103
104 let client = ToolingClient::new(mock_server.uri(), "test-token").unwrap();
105 let result: busbar_sf_client::QueryResult<serde_json::Value> = client
106 .query_all_records("SELECT Id, Name FROM ApexClass WHERE IsDeleted = true")
107 .await
108 .expect("should succeed");
109 assert_eq!(result.total_size, 2);
110 assert_eq!(result.records.len(), 2);
111 assert!(result.done);
112 }
113
114 #[tokio::test]
115 async fn test_search_wiremock() {
116 use wiremock::matchers::{method, path_regex};
117 use wiremock::{Mock, MockServer, ResponseTemplate};
118
119 let mock_server = MockServer::start().await;
120 let body = serde_json::json!({
121 "searchRecords": [
122 {
123 "Id": "01p000000000001AAA",
124 "attributes": {
125 "type": "ApexClass",
126 "url": "/services/data/v62.0/tooling/sobjects/ApexClass/01p000000000001AAA"
127 }
128 }
129 ]
130 });
131
132 Mock::given(method("GET"))
133 .and(path_regex(".*/tooling/search/"))
134 .respond_with(ResponseTemplate::new(200).set_body_json(&body))
135 .mount(&mock_server)
136 .await;
137
138 let client = ToolingClient::new(mock_server.uri(), "test-token").unwrap();
139 let result: crate::types::SearchResult<serde_json::Value> = client
140 .search("FIND {test} IN ALL FIELDS RETURNING ApexClass(Id, Name)")
141 .await
142 .expect("should succeed");
143 assert_eq!(result.search_records.len(), 1);
144 }
145}