vtiger_client/
client.rs

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