vtiger_client/
client.rs

1use csv::{Writer, WriterBuilder};
2use futures::stream::{self, StreamExt};
3use indexmap::IndexMap;
4use reqwest::{self, Response};
5use std::collections::HashMap;
6use std::{fs::File, io::Write};
7use tokio::sync::mpsc;
8
9use crate::types::{ExportFormat, VtigerQueryResponse, VtigerResponse};
10/// The base endpoint path for the Vtiger REST API.
11///
12/// This path is appended to the Vtiger instance URL to form the complete
13/// API endpoint. All REST API operations use this base path.
14///
15/// # Example
16///
17/// With a Vtiger instance at `https://example.vtiger.com/`, the full
18/// API URL would be: `https://example.vtiger.com/restapi/v1/vtiger/default`
19const LINK_ENDPOINT: &str = "restapi/v1/vtiger/default";
20
21/// Client for interacting with the Vtiger REST API.
22///
23/// This struct provides a high-level interface for making authenticated requests
24/// to a Vtiger CRM instance. It handles authentication, request formatting, and
25/// response parsing automatically.
26///
27/// # Examples
28///
29/// ```no_run
30///
31/// # #[tokio::main]
32/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
33///
34///     use vtiger_client::Vtiger;
35///
36///     let vtiger = Vtiger::new(
37///         "https://your-instance.vtiger.com",
38///         "your_username",
39///         "your_access_key"
40///     );
41///
42///     // Query records
43///     let response = vtiger.query("SELECT * FROM Leads LIMIT 10").await?;
44///     Ok(())
45/// }
46/// ```
47///
48/// # Authentication
49///
50/// Authentication uses the Vtiger username and access key. The access key can be
51/// found in your Vtiger user preferences under "My Preferences" > "Security".
52#[derive(Debug)]
53pub struct Vtiger {
54    /// The base URL of the Vtiger instance
55    url: String,
56    /// Username for API authentication
57    username: String,
58    /// Access key for API authentication (found in user preferences)
59    access_key: String,
60    /// HTTP client for making requests
61    client: reqwest::Client,
62}
63
64impl Vtiger {
65    /// Creates a new Vtiger API client.
66    ///
67    /// # Arguments
68    ///
69    /// * `url` - The base URL of your Vtiger instance (e.g., `https://your-instance.vtiger.com`)
70    /// * `username` - Your Vtiger username
71    /// * `access_key` - Your Vtiger access key (found in My Preferences > Security)
72    ///
73    /// # Examples
74    ///
75    /// ```no_run
76    /// use vtiger_client::Vtiger;
77    ///
78    /// let vtiger = Vtiger::new(
79    ///     "https://demo.vtiger.com",
80    ///     "admin",
81    ///     "your_access_key_here"
82    /// );
83    /// ```
84    pub fn new(url: &str, username: &str, access_key: &str) -> Self {
85        Vtiger {
86            url: url.to_string(),
87            username: username.to_string(),
88            access_key: access_key.to_string(),
89            client: reqwest::Client::new(),
90        }
91    }
92
93    /// Performs an authenticated GET request to the Vtiger API.
94    ///
95    /// This is an internal method that handles authentication and common headers
96    /// for all GET operations. The URL is constructed by combining the base URL,
97    /// the API endpoint path, and the provided endpoint.
98    ///
99    /// # Arguments
100    ///
101    /// * `endpoint` - The specific API endpoint path (e.g., "/query")
102    /// * `query` - Query parameters as key-value pairs
103    ///
104    /// # Returns
105    ///
106    /// Returns a `Result` containing the HTTP response or a request error.
107    async fn get(
108        &self,
109        endpoint: &str,
110        query: &[(&str, &str)],
111    ) -> Result<Response, reqwest::Error> {
112        let url = format!("{}{}{}", self.url, LINK_ENDPOINT, endpoint);
113        let mut request = self.client.get(&url);
114        if !query.is_empty() {
115            request = request.query(query);
116        }
117        request
118            .basic_auth(&self.username, Some(&self.access_key))
119            .header("Content-Type", "application/json")
120            .header("Accept", "application/json")
121            .send()
122            .await
123    }
124
125    /// Performs an authenticated POST request to the Vtiger API.
126    ///
127    /// This is an internal method that handles authentication and common headers
128    /// for all POST operations. The URL is constructed by combining the base URL,
129    /// the API endpoint path, and the provided endpoint.
130    ///
131    /// # Arguments
132    ///
133    /// * `endpoint` - The specific API endpoint path (e.g., "/create")
134    /// * `query` - Query parameters as key-value pairs
135    ///
136    /// # Returns
137    ///
138    /// Returns a `Result` containing the HTTP response or a request error.
139    async fn post(
140        &self,
141        endpoint: &str,
142        query: &[(&str, &str)],
143    ) -> Result<Response, reqwest::Error> {
144        let url = format!("{}{}{}", self.url, LINK_ENDPOINT, endpoint);
145        let mut request = self.client.post(&url);
146        if !query.is_empty() {
147            request = request.query(query);
148        }
149        request
150            .basic_auth(&self.username, Some(&self.access_key))
151            .header("Content-Type", "application/json")
152            .header("Accept", "application/json")
153            .send()
154            .await
155    }
156
157    /// Retrieves information about the currently authenticated user.
158    ///
159    /// This method calls the `/me` endpoint to get details about the user account
160    /// associated with the provided credentials. It's useful for verifying authentication
161    /// and retrieving user-specific information.
162    ///
163    /// # Returns
164    ///
165    /// Returns a `Result` containing a [`VtigerResponse`] with user information on success,
166    /// or a `reqwest::Error` if the request fails.
167    ///
168    /// # Examples
169    ///
170    /// ```no_run
171    /// use vtiger_client::Vtiger;
172    ///
173    /// #[tokio::main]
174    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
175    ///     let vtiger = Vtiger::new(
176    ///         "https://demo.vtiger.com",
177    ///         "admin",
178    ///         "your_access_key",
179    ///     );
180    ///
181    ///     let user_info = vtiger.me().await?;
182    ///     if user_info.success {
183    ///         println!("Authenticated as: {:?}", user_info.result);
184    ///     } else {
185    ///         eprintln!("Authentication failed: {:?}", user_info.error);
186    ///     }
187    ///     Ok(())
188    /// }
189    /// ```
190    ///
191    /// # Errors
192    ///
193    /// This method will return an error if:
194    /// - The network request fails
195    /// - The response cannot be parsed as JSON
196    /// - The server returns an invalid response format
197    pub async fn me(&self) -> Result<VtigerResponse, reqwest::Error> {
198        let response = self.get("/me", &[]).await?;
199        let vtiger_response = response.json::<VtigerResponse>().await?;
200        Ok(vtiger_response)
201    }
202
203    /// List the available modules and their information
204    ///
205    /// This endpoint returns a list of all of the available modules (like Contacts, Leads, Accounts)
206    /// available in your vtiger instance. You can optionally filter modules by the types of fields
207    /// they contain.
208    ///
209    /// #Arguments
210    ///
211    /// * `query`: An optional query string to filter the modules by. Common parameters:
212    /// - `("fieldTypeList", "null") or &[]- Return all available modules
213    /// - `("fieldTypeList", "picklist")- Return modules with picklist fields
214    /// - `("fieldTypeList", "grid")- Return modules with grid fields
215    ///
216    /// # Returns
217    ///
218    /// A `VtigerResponse` containing module information. The `result` typically includes:
219    /// - `types`: Array of module names
220    /// - `information`: Object with detailed info about each module
221    ///
222    /// # Errors
223    ///
224    /// Returns `reqwest::Error` for network issues or authentication failures.
225    /// Check the response's `success` field and `error` field for API-level errors.
226    ///
227    /// # Examples
228    ///
229    /// ```no_run
230    /// # use vtiger_client::Vtiger;
231    /// # #[tokio::main]
232    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
233    /// let vtiger = Vtiger::new("https://demo.vtiger.com/", "admin", "access_key");
234    ///
235    /// // Get all available modules
236    /// let all_modules = vtiger.list_types(&[("fieldTypeList", "null")]).await?;
237    ///
238    /// // Get modules with picklist fields only
239    /// let picklist_modules = vtiger.list_types(&[("fieldTypeList[]", "picklist")]).await?;
240    ///
241    /// // Get all modules (no filtering)
242    /// let modules = vtiger.list_types(&[]).await?;
243    ///
244    /// if all_modules.success {
245    ///     println!("Available modules: {:#?}", all_modules.result);
246    /// }
247    /// # Ok(())
248    /// # }
249    /// ```
250    pub async fn list_types(
251        &self,
252        query: &[(&str, &str)],
253    ) -> Result<VtigerResponse, reqwest::Error> {
254        let response = self.get("/listtypes", query).await?;
255        let vtiger_response = response.json::<VtigerResponse>().await?;
256        Ok(vtiger_response)
257    }
258    /// List the fields of a module.
259    ///
260    /// This endpoint returns a list of fields, their types, labels, default values, and other metadata of a module.
261    ///
262    /// #Arguments
263    ///
264    /// - `module_name`: The name of the module to describe.
265    ///
266    /// #Errors
267    ///
268    /// Returns `reqwest::Error` for network issues or authentication failures.
269    /// Check the response's `success` field and `error` field for API-level errors.
270    ///
271    /// #Examples
272    ///
273    /// ```no_run
274    /// # use vtiger_client::Vtiger;
275    /// # #[tokio::main]
276    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
277    /// let vtiger = Vtiger::new("https://demo.vtiger.com/", "admin", "access_key");
278    ///
279    /// // Describe the Accounts module
280    /// let accounts_description = vtiger.describe("Accounts").await?;
281    ///
282    /// if accounts_description.success {
283    ///     println!("Fields of Accounts module: {:#?}", accounts_description.result);
284    /// }
285    /// # Ok(())
286    /// # }
287    /// ```
288    pub async fn describe(&self, module_name: &str) -> Result<VtigerResponse, reqwest::Error> {
289        let response = self
290            .get("/describe", &[("elementType", module_name)])
291            .await?;
292        let vtiger_response = response.json::<VtigerResponse>().await?;
293        Ok(vtiger_response)
294    }
295
296    /// Export records from a module to multiple file formats.
297    ///
298    /// This method performs a high-performance, concurrent export of records from a Vtiger module.
299    /// It automatically handles batching, concurrency, and multiple output formats simultaneously.
300    /// Be wary of rate limits and potential API throttling.
301    ///
302    /// # Process Overview
303    ///
304    /// 1. **Count Phase**: Determines record counts for each filter
305    /// 2. **Batch Phase**: Creates work items based on batch size
306    /// 3. **Concurrent Export**: Runs multiple queries in parallel
307    /// 4. **File Writing**: Writes to multiple formats simultaneously
308    ///
309    /// # Arguments
310    ///
311    /// * `module` - The Vtiger module name (e.g., "Leads", "Contacts", "Accounts")
312    /// * `query_filter` - Optional filter as (column_name, filter_values). Uses LIKE queries with '-' suffix
313    /// * `batch_size` - Number of records per batch (recommended: 100-200)
314    /// * `concurrency` - Number of concurrent requests (recommended: 2-5)
315    /// * `format` - Vector of output formats to generate simultaneously
316    ///
317    /// # Output Files
318    ///
319    /// * **JSON**: `output.json` - Single JSON array with all records
320    /// * **JSON Lines**: `output.jsonl` - One JSON object per line
321    /// * **CSV**: `output.csv` - Traditional CSV with headers
322    ///
323    /// # Performance Notes
324    ///
325    /// * Vtiger's query API has an implicit limit of ~100 records per query
326    /// * Batch sizes of 200 may work depending on module and account tier
327    /// * Higher concurrency may hit API rate limits
328    /// * Multiple formats are written simultaneously for efficiency
329    ///
330    /// # Examples
331    ///
332    /// ```no_run
333    /// use vtiger_client::{Vtiger, ExportFormat};
334    ///
335    /// #[tokio::main]
336    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
337    ///     let vtiger = Vtiger::new("https://demo.vtiger.com", "admin", "key");
338    ///
339    ///     // Export all leads
340    ///     vtiger.export(
341    ///         "Leads",
342    ///         None,
343    ///         200,
344    ///         3,
345    ///         vec![ExportFormat::Json, ExportFormat::CSV]
346    ///     ).await?;
347    ///
348    ///     // Export leads with specific locations
349    ///     let locations = vec!["Los Angeles".to_string(), "Seattle".to_string()];
350    ///     vtiger.export(
351    ///         "Leads",
352    ///         Some(("Location", locations)),
353    ///         200,
354    ///         3,
355    ///         vec![ExportFormat::JsonLines]
356    ///     ).await?;
357    ///
358    ///     Ok(())
359    /// }
360    /// ```
361    ///
362    /// # Errors
363    ///
364    /// Returns an error if:
365    /// * Network requests fail
366    /// * File creation fails
367    /// * JSON serialization fails
368    /// * Invalid module name or query syntax
369    pub async fn export(
370        &self,
371        module: &str,
372        query_filter: Option<(&str, Vec<String>)>,
373        batch_size: usize,
374        concurrency: usize,
375        format: Vec<ExportFormat>,
376    ) -> Result<(), Box<dyn std::error::Error>> {
377        let (column, query_filters) = match query_filter {
378            Some((column, filters)) => (column, filters),
379            None => ("", vec![]),
380        };
381
382        let query_filter_counts: Vec<(String, i32)> = stream::iter(query_filters)
383            .map(|query_filter| async move {
384                let query = format!(
385                    "SELECT count(*) from {} WHERE {} LIKE '{}-%';",
386                    module, column, query_filter
387                );
388
389                let count = match self.query(&query).await {
390                    Ok(result) => match result.result {
391                        Some(records) => {
392                            // Extract the count value directly without chaining references
393                            if let Some(first_record) = records.first() {
394                                if let Some(count_val) = first_record.get("count") {
395                                    if let Some(count_str) = count_val.as_str() {
396                                        count_str.parse::<i32>().unwrap_or(0)
397                                    } else {
398                                        0
399                                    }
400                                } else {
401                                    0
402                                }
403                            } else {
404                                0
405                            }
406                        }
407                        None => 0,
408                    },
409                    Err(_) => 0,
410                };
411                if count != 0 {
412                    println!("Received {} records for {}", count, query_filter);
413                }
414                (query_filter, count)
415            })
416            .buffer_unordered(concurrency)
417            .collect()
418            .await;
419
420        let work_items = query_filter_counts
421            .into_iter()
422            .flat_map(|(query_filter, count)| {
423                (0..count)
424                    .step_by(batch_size)
425                    .map(|offset| (query_filter.clone(), offset, batch_size))
426                    .collect::<Vec<_>>()
427            })
428            .collect::<Vec<_>>();
429        let (record_sender, mut record_receiver) =
430            mpsc::unbounded_channel::<IndexMap<String, serde_json::Value>>();
431
432        let writer_task = tokio::spawn(async move {
433            let mut file_json_lines: Option<File> = None;
434            let mut file_json: Option<File> = None;
435            let mut file_csv: Option<Writer<File>> = None;
436            if format.contains(&ExportFormat::JsonLines) {
437                file_json_lines = Some(File::create("output.jsonl")?);
438            }
439            if format.contains(&ExportFormat::Json) {
440                file_json = Some(File::create("output.json")?);
441                if let Some(file_json) = file_json.as_mut()
442                    && let Err(e) = file_json.write_all(b"[")
443                {
444                    eprintln!("Could not write JSON array start: {e}");
445                }
446            }
447            if format.contains(&ExportFormat::CSV) {
448                file_csv = Some(
449                    WriterBuilder::new()
450                        .quote(b'"')
451                        .quote_style(csv::QuoteStyle::Always)
452                        .escape(b'\\')
453                        .terminator(csv::Terminator::CRLF)
454                        .from_path("output.csv")?,
455                );
456            }
457            let mut count = 0;
458
459            while let Some(record) = record_receiver.recv().await {
460                if let Some(ref mut file) = file_json_lines {
461                    writeln!(file, "{}", serde_json::to_string(&record)?)?;
462                }
463                if let Some(ref mut file) = file_json {
464                    if count != 0 {
465                        writeln!(file, ",")?;
466                    }
467                    writeln!(file, "{}", serde_json::to_string_pretty(&record)?)?;
468                }
469                if let Some(ref mut file) = file_csv {
470                    if count == 0 {
471                        let header: Vec<&str> = record.keys().map(|k| k.as_str()).collect();
472                        file.write_record(header)?;
473                    }
474                    let values: Vec<String> = record
475                        .values()
476                        .map(|v| match v {
477                            serde_json::Value::String(s) => s
478                                .replace('\n', "\\n")
479                                .replace('\r', "\\r")
480                                .replace('\t', "\\t"),
481                            serde_json::Value::Number(s) => s.to_string(),
482                            serde_json::Value::Bool(b) => b.to_string(),
483                            serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
484                                serde_json::to_string(v).unwrap_or_else(|_| String::new())
485                            }
486                            _ => "".to_string(),
487                        })
488                        .collect();
489                    file.write_record(values)?;
490                }
491                count += 1;
492                if count % 10000 == 0 {
493                    println!("Processed {} records", count);
494                }
495            }
496            println!("Finished writing {} total records", count);
497
498            if let Some(mut file) = file_json_lines {
499                file.flush()?;
500            }
501
502            if let Some(mut file) = file_json {
503                write!(file, "\n]")?; // Close the JSON array
504                file.flush()?;
505            }
506
507            if let Some(mut file) = file_csv {
508                file.flush()?;
509            }
510
511            println!("All files flushed and closed");
512            Ok::<_, std::io::Error>(())
513        });
514
515        stream::iter(work_items)
516            .map(|(query_filter, offset, batch_size)| {
517                let sender = record_sender.clone();
518                async move {
519                    let query = format!(
520                        "SELECT * FROM {} WHERE {} LIKE '{}%' LIMIT {}, {};",
521                        module, column, query_filter, offset, batch_size
522                    );
523                    println!("Executing query: {}", query);
524
525                    if let Ok(result) = self.query(&query).await
526                        && let Some(records) = result.result
527                    {
528                        let record_count = records.len();
529                        for record in records {
530                            if sender.send(record).is_err() {
531                                eprintln!("Failed to send record, writer may have stopped");
532                                break;
533                            }
534                        }
535                        println!(
536                            "Sent {} records from {} offset {}",
537                            record_count, query_filter, offset
538                        );
539                    }
540                    Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
541                }
542            })
543            .buffer_unordered(concurrency)
544            .collect::<Vec<_>>()
545            .await;
546
547        drop(record_sender);
548
549        let _ = writer_task.await?;
550
551        Ok(())
552    }
553
554    /// Create a new record in the specified module.
555    ///
556    /// This method creates a single record in Vtiger by sending field data
557    /// to the `/create` endpoint. The fields are automatically serialized
558    /// to JSON format as required by the Vtiger API.
559    ///
560    /// # Arguments
561    ///
562    /// * `module_name` - The name of the module to create the record in
563    /// * `fields` - Array of field name/value pairs to set on the new record
564    ///
565    /// # Returns
566    ///
567    /// Returns a [`VtigerResponse`] containing the created record's ID and data
568    /// on success, or the error details on failure.
569    ///
570    /// # Examples
571    ///
572    /// ```no_run
573    /// use vtiger_client::Vtiger;
574    ///
575    /// #[tokio::main]
576    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
577    ///     let vtiger = Vtiger::new("https://demo.vtiger.com", "admin", "key");
578    ///
579    ///     // Create a new lead
580    ///     let response = vtiger.create(
581    ///         "Leads",
582    ///         &[
583    ///             ("lastname", "Smith"),
584    ///             ("firstname", "John"),
585    ///             ("email", "john.smith@example.com"),
586    ///             ("company", "Acme Corp"),
587    ///         ]
588    ///     ).await?;
589    ///
590    ///     if response.success {
591    ///         println!("Created record: {:?}", response.result);
592    ///     } else {
593    ///         eprintln!("Creation failed: {:?}", response.error);
594    ///     }
595    ///
596    ///     Ok(())
597    /// }
598    /// ```
599    ///
600    /// # Errors
601    ///
602    /// This method will return an error if:
603    /// * The network request fails
604    /// * The response cannot be parsed as JSON
605    /// * Invalid module name or field names are provided
606    /// * Required fields are missing (check API response for details)
607    pub async fn create(
608        &self,
609        module_name: &str,
610        fields: &[(&str, &str)],
611    ) -> Result<VtigerResponse, Box<dyn std::error::Error>> {
612        let fields_map: HashMap<&str, &str> = fields.iter().cloned().collect();
613        let element_json = serde_json::to_string(&fields_map)
614            .map_err(|e| format!("Failed to serialize string IndexMap to JSON: {e}"))?;
615
616        let response = self
617            .post(
618                "/create",
619                &[("elementType", module_name), ("element", &element_json)],
620            )
621            .await?;
622        let vtiger_response = response.json::<VtigerResponse>().await?;
623        Ok(vtiger_response)
624    }
625
626    /// Retrieve a single record by its ID.
627    ///
628    /// This method fetches a complete record from Vtiger using its unique ID.
629    /// The ID should be in Vtiger's format (e.g., "12x34" where 12 is the
630    /// module ID and 34 is the record ID).
631    ///
632    /// # Arguments
633    ///
634    /// * `record_id` - The Vtiger record ID (format: "ModuleIDxRecordID")
635    ///
636    /// # Returns
637    ///
638    /// Returns a [`VtigerResponse`] containing the record data on success,
639    /// or error details on failure.
640    ///
641    /// # Examples
642    ///
643    /// ```no_run
644    /// use vtiger_client::Vtiger;
645    ///
646    /// #[tokio::main]
647    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
648    ///     let vtiger = Vtiger::new("https://demo.vtiger.com", "admin", "key");
649    ///
650    ///     // Retrieve a specific lead record
651    ///     let response = vtiger.retrieve("12x34").await?;
652    ///
653    ///     if response.success {
654    ///         if let Some(record) = response.result {
655    ///             println!("Record data: {:#?}", record);
656    ///             // Access specific fields
657    ///             if let Some(name) = record.get("lastname") {
658    ///                 println!("Last name: {}", name);
659    ///             }
660    ///         }
661    ///     } else {
662    ///         eprintln!("Retrieval failed: {:?}", response.error);
663    ///     }
664    ///
665    ///     Ok(())
666    /// }
667    /// ```
668    ///
669    /// # Record ID Format
670    ///
671    /// Vtiger record IDs follow the format "ModuleIDxRecordID":
672    /// * `12x34` - Module 12, Record 34
673    /// * `4x567` - Module 4, Record 567
674    ///
675    /// You can typically get these IDs from:
676    /// * Previous create/query operations
677    /// * The Vtiger web interface URL
678    /// * Other API responses
679    ///
680    /// # Errors
681    ///
682    /// This method will return an error if:
683    /// * The network request fails
684    /// * The response cannot be parsed as JSON
685    /// * The record ID format is invalid
686    /// * The record doesn't exist or you don't have permission to view it
687    pub async fn retrieve(&self, record_id: &str) -> Result<VtigerResponse, reqwest::Error> {
688        let response = self.get("/retrieve", &[("id", record_id)]).await?;
689        let vtiger_response = response.json::<VtigerResponse>().await?;
690        Ok(vtiger_response)
691    }
692
693    /// Execute a SQL-like query against Vtiger data.
694    ///
695    /// This method allows you to query Vtiger records using a SQL-like syntax.
696    /// It returns multiple records and is the primary method for searching
697    /// and filtering data in Vtiger.
698    ///
699    /// # Arguments
700    ///
701    /// * `query` - SQL-like query string using Vtiger's query syntax
702    ///
703    /// # Returns
704    ///
705    /// Returns a [`VtigerQueryResponse`] containing an array of matching records
706    /// on success, or error details on failure.
707    ///
708    /// # Query Syntax
709    ///
710    /// Vtiger supports a subset of SQL:
711    /// * `SELECT * FROM ModuleName`
712    /// * `SELECT field1, field2 FROM ModuleName`
713    /// * `WHERE` conditions with `=`, `!=`, `LIKE`, `IN`
714    /// * `ORDER BY` for sorting
715    /// * `LIMIT` for pagination (recommended: ≤200 records)
716    ///
717    /// # Examples
718    ///
719    /// ```no_run
720    /// use vtiger_client::Vtiger;
721    ///
722    /// #[tokio::main]
723    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
724    ///     let vtiger = Vtiger::new("https://demo.vtiger.com", "admin", "key");
725    ///
726    ///     // Basic query
727    ///     let response = vtiger.query("SELECT * FROM Leads LIMIT 10").await?;
728    ///
729    ///     // Query with conditions
730    ///     let response = vtiger.query(
731    ///         "SELECT firstname, lastname, email FROM Leads WHERE email != ''"
732    ///     ).await?;
733    ///
734    ///     // Query with LIKE operator
735    ///     let response = vtiger.query(
736    ///         "SELECT * FROM Leads WHERE lastname LIKE 'Smith%'"
737    ///     ).await?;
738    ///
739    ///     // Query with ordering
740    ///     let response = vtiger.query(
741    ///         "SELECT * FROM Leads ORDER BY createdtime DESC LIMIT 50"
742    ///     ).await?;
743    ///
744    ///     if response.success {
745    ///         if let Some(records) = response.result {
746    ///             println!("Found {} records", records.len());
747    ///             for record in records {
748    ///                 println!("Record: {:#?}", record);
749    ///             }
750    ///         }
751    ///     } else {
752    ///         eprintln!("Query failed: {:?}", response.error);
753    ///     }
754    ///
755    ///     Ok(())
756    /// }
757    /// ```
758    ///
759    /// # Performance Considerations
760    ///
761    /// * **Limit results**: Always use `LIMIT` to avoid timeouts
762    /// * **Batch large queries**: Use pagination for large datasets
763    /// * **Index usage**: Filter on indexed fields when possible
764    /// * **Concurrent queries**: Use multiple queries for better performance
765    ///
766    /// # Common Query Patterns
767    ///
768    /// ```sql
769    /// -- Get recent records
770    /// SELECT * FROM Leads ORDER BY createdtime DESC LIMIT 100
771    ///
772    /// -- Filter by custom field
773    /// SELECT * FROM Leads WHERE location LIKE 'Los Angeles%'
774    ///
775    /// -- Count records
776    /// SELECT count(*) FROM Leads WHERE leadstatus = 'Hot'
777    ///
778    /// -- Multiple conditions
779    /// SELECT * FROM Leads WHERE email != '' AND leadstatus IN ('Hot', 'Warm')
780    /// ```
781    ///
782    /// # Errors
783    ///
784    /// This method will return an error if:
785    /// * The network request fails
786    /// * The response cannot be parsed as JSON
787    /// * Invalid SQL syntax is used
788    /// * Referenced fields or modules don't exist
789    /// * Query exceeds time or result limits
790    pub async fn query(&self, query: &str) -> Result<VtigerQueryResponse, reqwest::Error> {
791        let response = self.get("/query", &[("query", query)]).await?;
792        let vtiger_response = response.json::<VtigerQueryResponse>().await?;
793        Ok(vtiger_response)
794    }
795
796    /// Update a record in the database
797    ///
798    /// This function takes a single record that must include an ID and all
799    /// required fields on the module in order to successfulyy update the record.
800    /// It's recommended to use the revise function instead of this one.
801    ///
802    /// # Arguments
803    ///
804    /// * `fields` - A vector of tuples containing the field name and value to update.
805    ///
806    /// # Returns
807    ///
808    /// Returns a [`VtigerResponse`] containing a message indicating success or failure,
809    /// and the result if successful.
810    ///
811    /// # Examples
812    ///
813    /// ```no_run
814    /// use vtiger_client::Vtiger;
815    ///
816    /// #[tokio::main]
817    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
818    ///     let vtiger = Vtiger::new("https://demo.vtiger.com", "admin", "key");
819    ///
820    ///     // Query with conditions
821    ///     let update_fields = vec![
822    ///         ("id", "2x12345"),
823    ///         ("first_name", "John"),
824    ///         ("last_name", "Smith"),
825    ///         ("email", "smith@example.com"),
826    ///         ("phone", "604-555-1212"),
827    ///         ("lane", "123 Main St"),
828    ///         ("city", "Los Angeles"),
829    ///         ("state", "CA"),
830    ///         ("postal_code", "12345"),
831    ///     ];
832    ///     let response = vtiger.update(&update_fields).await?;
833    ///
834    ///     println!("Updated record: {:?}", response);
835    ///     Ok(())
836    /// }
837    /// ```
838    pub async fn update(
839        &self,
840        fields: &Vec<(&str, &str)>,
841    ) -> Result<VtigerResponse, Box<dyn std::error::Error>> {
842        let map: HashMap<_, _> = fields.iter().cloned().collect();
843        let fields_json = serde_json::to_string(&map)?;
844
845        let response = self.post("/update", &[("element", &fields_json)]).await?;
846        let vtiger_response = response.json::<VtigerResponse>().await?;
847        Ok(vtiger_response)
848    }
849
850    /// Update a record in the database
851    ///
852    /// This function takes a single record that must include an ID and at least
853    /// one field to update.
854    ///
855    /// # Arguments
856    ///
857    /// * `fields` - A vector of tuples containing the field name and value to update.
858    ///
859    /// # Returns
860    ///
861    /// Returns a [`VtigerResponse`] containing a message indicating success or failure,
862    /// and the result if successful.
863    ///
864    /// # Examples
865    ///
866    /// ```no_run
867    /// use vtiger_client::Vtiger;
868    ///
869    /// #[tokio::main]
870    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
871    ///     let vtiger = Vtiger::new("https://demo.vtiger.com", "admin", "key");
872    ///
873    ///     // Query with conditions
874    ///     let update_fields = vec![
875    ///         ("id", "2x12345"),
876    ///         ("phone", "604-555-1212"),
877    ///     ];
878    ///     let response = vtiger.revise(&update_fields).await?;
879    ///
880    ///     println!("Updated record: {:?}", response);
881    ///     Ok(())
882    /// }
883    /// ```
884    pub async fn revise(
885        &self,
886        fields: &Vec<(&str, &str)>,
887    ) -> Result<VtigerResponse, Box<dyn std::error::Error>> {
888        let map: HashMap<_, _> = fields.iter().cloned().collect();
889        let fields_json = serde_json::to_string(&map)?;
890
891        let response = self.post("/revise", &[("element", &fields_json)]).await?;
892        let vtiger_response = response.json::<VtigerResponse>().await?;
893        Ok(vtiger_response)
894    }
895}