Skip to main content

queries/
queries.rs

1//! SOQL query examples with security best practices
2//!
3//! This example demonstrates:
4//! 1. QueryBuilder - Safe by default with fluent API (RECOMMENDED)
5//! 2. Type-safe structs vs dynamic JSON (both patterns shown)
6//! 3. Manual escaping (last resort - shown for completeness)
7//!
8//! Run with: cargo run --example queries
9
10use busbar_sf_auth::{Credentials, SalesforceCredentials};
11use busbar_sf_client::security::soql;
12use busbar_sf_rest::{QueryBuilder, SalesforceRestClient};
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15
16/// Account record for type-safe queries
17///
18/// Use typed structs when:
19/// - Building production applications  
20/// - You know the schema ahead of time
21/// - You want compile-time safety and IDE support
22#[derive(Debug, Serialize, Deserialize, Clone)]
23struct Account {
24    #[serde(rename = "Id")]
25    id: String,
26    #[serde(rename = "Name")]
27    name: String,
28    #[serde(rename = "Industry", skip_serializing_if = "Option::is_none")]
29    industry: Option<String>,
30}
31
32/// Contact record with relationship query support
33#[derive(Debug, Deserialize)]
34#[allow(dead_code)]
35struct Contact {
36    #[serde(rename = "Id")]
37    id: String,
38    #[serde(rename = "Name")]
39    name: String,
40    #[serde(rename = "Email")]
41    email: Option<String>,
42    #[serde(rename = "Account")]
43    account: Option<AccountRef>,
44}
45
46/// Nested Account reference in Contact
47#[derive(Debug, Deserialize)]
48#[allow(dead_code)]
49struct AccountRef {
50    #[serde(rename = "Name")]
51    name: String,
52}
53
54/// Aggregate query result
55#[derive(Debug, Deserialize)]
56#[allow(dead_code)]
57struct IndustryCount {
58    #[serde(rename = "Industry")]
59    industry: String,
60    #[serde(rename = "total")]
61    total: i32,
62}
63
64#[tokio::main]
65async fn main() -> Result<(), Box<dyn std::error::Error>> {
66    // Initialize tracing for better observability
67    tracing_subscriber::fmt::init();
68
69    println!("=== Salesforce SOQL Query Examples ===\n");
70
71    let creds = get_credentials().await?;
72    let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())?;
73
74    // RECOMMENDED: Use QueryBuilder (safe by default)
75    println!("--- QueryBuilder Pattern (RECOMMENDED) ---\n");
76    example_query_builder_typed(&client).await?;
77    example_query_builder_dynamic(&client).await?;
78    example_query_builder_advanced(&client).await?;
79
80    // Alternative: Raw queries (less safe, but flexible)
81    println!("\n--- Raw Query Patterns ---\n");
82    example_basic_query_typed(&client).await?;
83    example_basic_query_dynamic(&client).await?;
84
85    // Manual escaping (NOT recommended, but shown for completeness)
86    println!("\n--- Manual Escaping (NOT RECOMMENDED) ---\n");
87    example_manual_escaping(&client).await?;
88
89    // Advanced queries
90    println!("\n--- Advanced Queries ---\n");
91    example_relationship_query(&client).await?;
92    example_aggregate_query(&client).await?;
93
94    println!("\n✓ All query examples completed successfully!");
95
96    Ok(())
97}
98
99/// Example 1a: QueryBuilder with type-safe results (RECOMMENDED)
100async fn example_query_builder_typed(
101    client: &SalesforceRestClient,
102) -> Result<(), Box<dyn std::error::Error>> {
103    println!("Example 1a: QueryBuilder with Type Safety");
104    println!("------------------------------------------");
105
106    // Simulated user input (potentially dangerous)
107    let user_name = "O'Brien's Company"; // Has single quote
108
109    // Build and execute query with automatic escaping
110    let accounts: Vec<Account> = QueryBuilder::new("Account")?
111        .select(&["Id", "Name", "Industry"])
112        .where_eq("Name", user_name)? // Automatically escaped!
113        .limit(10)
114        .execute(client)
115        .await?;
116
117    println!("✓ Found {} accounts", accounts.len());
118    println!("  Benefits: Type-safe results, automatic escaping, fluent API");
119    println!();
120
121    Ok(())
122}
123
124/// Example 1b: QueryBuilder with dynamic JSON results
125async fn example_query_builder_dynamic(
126    client: &SalesforceRestClient,
127) -> Result<(), Box<dyn std::error::Error>> {
128    println!("Example 1b: QueryBuilder with Dynamic JSON");
129    println!("-------------------------------------------");
130
131    let user_pattern = "tech%value"; // Has SQL wildcards
132
133    // Works with HashMap for ergonomic access
134    let accounts: Vec<HashMap<String, serde_json::Value>> = QueryBuilder::new("Account")?
135        .select(&["Id", "Name", "Industry"])
136        .where_like("Name", user_pattern)? // Wildcards automatically escaped!
137        .limit(5)
138        .execute(client)
139        .await?;
140
141    println!("✓ Found {} accounts", accounts.len());
142    for account in accounts.iter().take(3) {
143        let name = account
144            .get("Name")
145            .and_then(|v| v.as_str())
146            .unwrap_or("Unknown");
147        println!("  - {}", name);
148    }
149    println!("  Benefits: Flexible, ergonomic HashMap access, no struct needed");
150    println!();
151
152    Ok(())
153}
154
155/// Example 1c: QueryBuilder with advanced features
156async fn example_query_builder_advanced(
157    client: &SalesforceRestClient,
158) -> Result<(), Box<dyn std::error::Error>> {
159    println!("Example 1c: QueryBuilder Advanced Features");
160    println!("-------------------------------------------");
161
162    let industries = vec!["Technology", "Finance"];
163
164    let accounts: Vec<Account> = QueryBuilder::new("Account")?
165        .select(&["Id", "Name", "Industry"])
166        .where_in("Industry", &industries)? // Multiple values
167        .order_by("Name", true)? // Sort ascending
168        .limit(20)
169        .execute(client)
170        .await?;
171
172    println!(
173        "✓ Found {} accounts in specified industries",
174        accounts.len()
175    );
176    println!("  Features: WHERE IN, ORDER BY, fluent chaining");
177    println!();
178
179    Ok(())
180}
181
182/// Example 1a: Basic type-safe SOQL query (RECOMMENDED for production)
183async fn example_basic_query_typed(
184    client: &SalesforceRestClient,
185) -> Result<(), Box<dyn std::error::Error>> {
186    println!("Example 1a: Type-Safe Query");
187    println!("----------------------------");
188
189    let query = "SELECT Id, Name, Industry FROM Account LIMIT 5";
190    let result: busbar_sf_client::QueryResult<Account> = client.query(query).await?;
191
192    println!(
193        "✓ Found {} accounts (total: {})",
194        result.records.len(),
195        result.total_size
196    );
197    for account in &result.records {
198        println!("  - {} (Industry: {:?})", account.name, account.industry);
199    }
200    println!("  Benefits: Type safety, field access without unwrapping");
201    println!();
202
203    Ok(())
204}
205
206/// Example 1b: Dynamic JSON query with proper serde_json patterns
207async fn example_basic_query_dynamic(
208    client: &SalesforceRestClient,
209) -> Result<(), Box<dyn std::error::Error>> {
210    println!("Example 1b: Dynamic JSON Query");
211    println!("-------------------------------");
212
213    let query = "SELECT Id, Name, Industry FROM Account LIMIT 5";
214
215    // Use HashMap for more ergonomic access than raw Value
216    let result: busbar_sf_client::QueryResult<HashMap<String, serde_json::Value>> =
217        client.query(query).await?;
218
219    println!("✓ Found {} accounts", result.records.len());
220    for account in &result.records {
221        // Much more ergonomic than account["Name"].as_str().unwrap_or()
222        let name = account
223            .get("Name")
224            .and_then(|v| v.as_str())
225            .unwrap_or("Unknown");
226        let industry = account
227            .get("Industry")
228            .and_then(|v| v.as_str())
229            .unwrap_or("None");
230        println!("  - {} (Industry: {})", name, industry);
231    }
232    println!("  Benefits: HashMap provides .get() method, no indexing panics");
233    println!();
234
235    Ok(())
236}
237
238/// Example 2: Automatic pagination with type safety
239#[allow(dead_code)]
240async fn example_query_pagination_typed(
241    client: &SalesforceRestClient,
242) -> Result<(), Box<dyn std::error::Error>> {
243    println!("Example 2: Pagination with Type Safety");
244    println!("---------------------------------------");
245
246    // query_all automatically handles pagination
247    let query = "SELECT Id, Name FROM Account LIMIT 100";
248    let accounts: Vec<Account> = client.query_all(query).await?;
249
250    println!(
251        "✓ Retrieved {} accounts (automatic pagination)",
252        accounts.len()
253    );
254    println!();
255
256    Ok(())
257}
258
259/// Example 3: Manual escaping - NOT RECOMMENDED but shown for completeness
260///
261/// WARNING: This approach is error-prone!
262/// - Easy to forget to escape
263/// - Easy to use wrong escape function (escape_string vs escape_like)
264/// - Not safe by default
265///
266/// Prefer the SafeQueryBuilder pattern shown above!
267async fn example_manual_escaping(
268    client: &SalesforceRestClient,
269) -> Result<(), Box<dyn std::error::Error>> {
270    println!("Example 3: Manual Escaping (NOT RECOMMENDED)");
271    println!("---------------------------------------------");
272    println!("⚠️  WARNING: Easy to forget! Use QueryBuilder instead.");
273    println!();
274
275    let user_input = "O'Brien's Company";
276    let malicious_input = "'; DELETE FROM Account--";
277
278    // Manual escaping - requires developer to remember!
279    let safe_name = soql::escape_string(user_input);
280    let query = format!("SELECT Id, Name FROM Account WHERE Name = '{}'", safe_name);
281
282    let accounts: Vec<Account> = client.query_all(&query).await?;
283    println!("  Found {} accounts", accounts.len());
284
285    // Show what happens if you forget to escape (DON'T DO THIS!)
286    let safe_malicious = soql::escape_string(malicious_input);
287    println!("\n  Injection attempt:");
288    println!("  Raw input:      {}", malicious_input);
289    println!("  After escaping: {}", safe_malicious);
290    println!("\n  ❌ Problem: Relies on developer remembering to escape");
291    println!("  ✅ Solution: Use QueryBuilder that escapes automatically");
292    println!();
293
294    Ok(())
295}
296
297/// Example 4: Field validation
298#[allow(dead_code)]
299async fn example_field_validation(
300    client: &SalesforceRestClient,
301) -> Result<(), Box<dyn std::error::Error>> {
302    println!("Example 4: Field Validation");
303    println!("---------------------------");
304
305    // User-provided field names (could be malicious)
306    let user_fields = vec![
307        "Id",
308        "Name",
309        "Industry",
310        "Bad'; DROP TABLE--", // Injection attempt
311        "CustomField__c",
312    ];
313
314    // Filter to only safe field names
315    let safe_fields: Vec<&str> = soql::filter_safe_fields(user_fields.iter().copied()).collect();
316
317    println!("  Original fields: {:?}", user_fields);
318    println!("  Safe fields:     {:?}", safe_fields);
319
320    // Build SELECT clause with safe fields
321    if let Some(select_clause) = soql::build_safe_select(&safe_fields) {
322        let query = format!("SELECT {} FROM Account LIMIT 5", select_clause);
323
324        // Use HashMap for dynamic field access
325        let result: busbar_sf_client::QueryResult<HashMap<String, serde_json::Value>> =
326            client.query(&query).await?;
327        println!("✓ Retrieved {} records", result.records.len());
328    } else {
329        println!("✗ No safe fields to query");
330    }
331
332    println!();
333
334    Ok(())
335}
336
337/// Example 7: Relationship query
338async fn example_relationship_query(
339    client: &SalesforceRestClient,
340) -> Result<(), Box<dyn std::error::Error>> {
341    println!("Example 7: Relationship Query");
342    println!("-----------------------------");
343
344    let query =
345        "SELECT Id, Name, Email, Account.Name FROM Contact WHERE Account.Name != null LIMIT 5";
346
347    let contacts: Vec<serde_json::Value> = client.query_all(query).await?;
348
349    println!("✓ Found {} contacts with accounts", contacts.len());
350    for contact in &contacts {
351        let name = contact
352            .get("Name")
353            .and_then(|v| v.as_str())
354            .unwrap_or("Unknown");
355        let id = contact
356            .get("Id")
357            .and_then(|v| v.as_str())
358            .unwrap_or("Unknown");
359        if let Some(account) = contact.get("Account") {
360            if let Some(account_name) = account.get("Name").and_then(|v| v.as_str()) {
361                println!("  - {} ({}) @ {}", name, id, account_name);
362            }
363        }
364    }
365    println!();
366
367    Ok(())
368}
369
370/// Example 8: Aggregate query
371async fn example_aggregate_query(
372    client: &SalesforceRestClient,
373) -> Result<(), Box<dyn std::error::Error>> {
374    println!("Example 8: Aggregate Query");
375    println!("--------------------------");
376
377    let query = "SELECT Industry, COUNT(Id) total FROM Account WHERE Industry != null GROUP BY Industry ORDER BY COUNT(Id) DESC LIMIT 5";
378
379    let results: Vec<serde_json::Value> = client.query_all(query).await?;
380
381    println!("✓ Top {} industries:", results.len());
382    for result in &results {
383        let industry = result
384            .get("Industry")
385            .and_then(|v| v.as_str())
386            .unwrap_or("Unknown");
387        let total = result.get("total").and_then(|v| v.as_i64()).unwrap_or(0);
388        println!("  - {}: {} accounts", industry, total);
389    }
390    println!();
391
392    Ok(())
393}
394
395/// Helper function to build secure queries
396#[allow(dead_code)]
397fn build_secure_query(
398    sobject: &str,
399    fields: &[&str],
400    where_field: &str,
401    user_value: &str,
402) -> Option<String> {
403    // Validate SObject name
404    if !soql::is_safe_sobject_name(sobject) {
405        return None;
406    }
407
408    // Validate and build field list
409    let select_clause = soql::build_safe_select(fields)?;
410
411    // Validate WHERE field
412    if !soql::is_safe_field_name(where_field) {
413        return None;
414    }
415
416    // Escape user value
417    let safe_value = soql::escape_string(user_value);
418
419    Some(format!(
420        "SELECT {} FROM {} WHERE {} = '{}'",
421        select_clause, sobject, where_field, safe_value
422    ))
423}
424
425/// Helper function to get credentials
426async fn get_credentials() -> Result<SalesforceCredentials, Box<dyn std::error::Error>> {
427    if let Ok(creds) = SalesforceCredentials::from_sfdx_alias("default").await {
428        println!("✓ Using credentials from Salesforce CLI\n");
429        return Ok(creds);
430    }
431
432    match SalesforceCredentials::from_env() {
433        Ok(creds) => {
434            println!("✓ Using credentials from environment variables\n");
435            Ok(creds)
436        }
437        Err(e) => {
438            eprintln!("✗ Failed to load credentials: {}", e);
439            eprintln!("\nPlease either:");
440            eprintln!("  1. Authenticate with Salesforce CLI: sf org login web");
441            eprintln!("  2. Set environment variables: SF_INSTANCE_URL, SF_ACCESS_TOKEN");
442            Err(e.into())
443        }
444    }
445}