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
388 let count = match self.query(&query).await {
389 Ok(result) => match result.result {
390 Some(records) => {
391 // Extract the count value directly without chaining references
392 if let Some(first_record) = records.get(0) {
393 if let Some(count_val) = first_record.get("count") {
394 if let Some(count_str) = count_val.as_str() {
395 count_str.parse::<i32>().unwrap_or(0)
396 } else {
397 0
398 }
399 } else {
400 0
401 }
402 } else {
403 0
404 }
405 }
406 None => 0,
407 },
408 Err(_) => 0,
409 };
410 if count != 0 {
411 println!("Received {} records for {}", count, query_filter);
412 }
413 (query_filter, count)
414 })
415 .buffer_unordered(concurrency)
416 .collect()
417 .await;
418
419 let work_items = query_filter_counts
420 .into_iter()
421 .flat_map(|(query_filter, count)| {
422 (0..count)
423 .step_by(batch_size.clone())
424 .map(|offset| (query_filter.clone(), offset, batch_size))
425 .collect::<Vec<_>>()
426 })
427 .collect::<Vec<_>>();
428 let (record_sender, mut record_receiver) =
429 mpsc::unbounded_channel::<IndexMap<String, serde_json::Value>>();
430
431 let writer_task = tokio::spawn(async move {
432 let mut file_json_lines: Option<File> = None;
433 let mut file_json: Option<File> = None;
434 let mut file_csv: Option<Writer<File>> = None;
435 if format.contains(&ExportFormat::JsonLines) {
436 file_json_lines = Some(File::create("output.jsonl").unwrap());
437 }
438 if format.contains(&ExportFormat::Json) {
439 file_json = Some(File::create("output.json").unwrap());
440 file_json
441 .as_mut()
442 .unwrap()
443 .write_all(b"[")
444 .expect("Could not write JSON array start");
445 }
446 if format.contains(&ExportFormat::CSV) {
447 file_csv = Some(
448 WriterBuilder::new()
449 .quote(b'"')
450 .quote_style(csv::QuoteStyle::Always)
451 .escape(b'\\')
452 .terminator(csv::Terminator::CRLF)
453 .from_path("output.csv")
454 .expect("CSV file creation failed"),
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).unwrap()).unwrap();
462 }
463 if let Some(ref mut file) = file_json {
464 if count != 0 {
465 write!(file, ",\n").unwrap();
466 }
467 writeln!(file, "{}", serde_json::to_string_pretty(&record).unwrap()).unwrap();
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 .expect("Could not write CSV header record!");
474 }
475 let values: Vec<String> = record
476 .values()
477 .map(|v| match v {
478 serde_json::Value::String(s) => s
479 .replace('\n', "\\n")
480 .replace('\r', "\\r")
481 .replace('\t', "\\t"),
482 serde_json::Value::Number(s) => s.to_string(),
483 serde_json::Value::Bool(b) => b.to_string(),
484 serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
485 serde_json::to_string(v).unwrap_or_else(|_| String::new())
486 }
487 _ => "".to_string(),
488 })
489 .collect();
490 file.write_record(values)
491 .expect("Could not write CSV values record@");
492 }
493 count += 1;
494 if count % 10000 == 0 {
495 println!("Processed {} records", count);
496 }
497 }
498 println!("Finished writing {} total records", count);
499
500 if let Some(mut file) = file_json_lines {
501 file.flush().unwrap();
502 }
503
504 if let Some(mut file) = file_json {
505 write!(file, "\n]").unwrap(); // Close the JSON array
506 file.flush().unwrap();
507 }
508
509 if let Some(mut file) = file_csv {
510 file.flush().unwrap();
511 }
512
513 println!("All files flushed and closed");
514 });
515
516 stream::iter(work_items)
517 .map(|(query_filter, offset, batch_size)| {
518 let sender = record_sender.clone();
519 async move {
520 let query = format!(
521 "SELECT * FROM {} WHERE {} LIKE '{}%' LIMIT {}, {};",
522 module, column, query_filter, offset, batch_size
523 );
524 println!("Executing query: {}", query);
525
526 if let Ok(result) = self.query(&query).await {
527 if let Some(records) = result.result {
528 let record_count = records.len();
529 for record in records {
530 if let Err(_) = sender.send(record) {
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 }
541 Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
542 }
543 })
544 .buffer_unordered(concurrency)
545 .collect::<Vec<_>>()
546 .await;
547
548 drop(record_sender);
549
550 writer_task.await.unwrap();
551
552 Ok(())
553 }
554
555 /// Create a new record in the specified module.
556 ///
557 /// This method creates a single record in Vtiger by sending field data
558 /// to the `/create` endpoint. The fields are automatically serialized
559 /// to JSON format as required by the Vtiger API.
560 ///
561 /// # Arguments
562 ///
563 /// * `module_name` - The name of the module to create the record in
564 /// * `fields` - Array of field name/value pairs to set on the new record
565 ///
566 /// # Returns
567 ///
568 /// Returns a [`VtigerResponse`] containing the created record's ID and data
569 /// on success, or the error details on failure.
570 ///
571 /// # Examples
572 ///
573 /// ```no_run
574 /// use vtiger_client::Vtiger;
575 ///
576 /// #[tokio::main]
577 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
578 /// let vtiger = Vtiger::new("https://demo.vtiger.com", "admin", "key");
579 ///
580 /// // Create a new lead
581 /// let response = vtiger.create(
582 /// "Leads",
583 /// &[
584 /// ("lastname", "Smith"),
585 /// ("firstname", "John"),
586 /// ("email", "john.smith@example.com"),
587 /// ("company", "Acme Corp"),
588 /// ]
589 /// ).await?;
590 ///
591 /// if response.success {
592 /// println!("Created record: {:?}", response.result);
593 /// } else {
594 /// eprintln!("Creation failed: {:?}", response.error);
595 /// }
596 ///
597 /// Ok(())
598 /// }
599 /// ```
600 ///
601 /// # Errors
602 ///
603 /// This method will return an error if:
604 /// * The network request fails
605 /// * The response cannot be parsed as JSON
606 /// * Invalid module name or field names are provided
607 /// * Required fields are missing (check API response for details)
608 pub async fn create(
609 &self,
610 module_name: &str,
611 fields: &[(&str, &str)],
612 ) -> Result<VtigerResponse, reqwest::Error> {
613 let fields_map: IndexMap<&str, &str> = fields.iter().cloned().collect();
614 let element_json = serde_json::to_string(&fields_map)
615 .expect("Failed to serialize string IndexMap to JSON");
616
617 let response = self
618 .post(
619 "/create",
620 &[("elementType", module_name), ("element", &element_json)],
621 )
622 .await?;
623 let vtiger_response = response.json::<VtigerResponse>().await?;
624 Ok(vtiger_response)
625 }
626
627 /// Retrieve a single record by its ID.
628 ///
629 /// This method fetches a complete record from Vtiger using its unique ID.
630 /// The ID should be in Vtiger's format (e.g., "12x34" where 12 is the
631 /// module ID and 34 is the record ID).
632 ///
633 /// # Arguments
634 ///
635 /// * `record_id` - The Vtiger record ID (format: "ModuleIDxRecordID")
636 ///
637 /// # Returns
638 ///
639 /// Returns a [`VtigerResponse`] containing the record data on success,
640 /// or error details on failure.
641 ///
642 /// # Examples
643 ///
644 /// ```no_run
645 /// use vtiger_client::Vtiger;
646 ///
647 /// #[tokio::main]
648 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
649 /// let vtiger = Vtiger::new("https://demo.vtiger.com", "admin", "key");
650 ///
651 /// // Retrieve a specific lead record
652 /// let response = vtiger.retrieve("12x34").await?;
653 ///
654 /// if response.success {
655 /// if let Some(record) = response.result {
656 /// println!("Record data: {:#?}", record);
657 /// // Access specific fields
658 /// if let Some(name) = record.get("lastname") {
659 /// println!("Last name: {}", name);
660 /// }
661 /// }
662 /// } else {
663 /// eprintln!("Retrieval failed: {:?}", response.error);
664 /// }
665 ///
666 /// Ok(())
667 /// }
668 /// ```
669 ///
670 /// # Record ID Format
671 ///
672 /// Vtiger record IDs follow the format "ModuleIDxRecordID":
673 /// * `12x34` - Module 12, Record 34
674 /// * `4x567` - Module 4, Record 567
675 ///
676 /// You can typically get these IDs from:
677 /// * Previous create/query operations
678 /// * The Vtiger web interface URL
679 /// * Other API responses
680 ///
681 /// # Errors
682 ///
683 /// This method will return an error if:
684 /// * The network request fails
685 /// * The response cannot be parsed as JSON
686 /// * The record ID format is invalid
687 /// * The record doesn't exist or you don't have permission to view it
688 pub async fn retrieve(&self, record_id: &str) -> Result<VtigerResponse, reqwest::Error> {
689 let response = self
690 .get(&format!("/retrieve"), &[("id", record_id)])
691 .await?;
692 let vtiger_response = response.json::<VtigerResponse>().await?;
693 Ok(vtiger_response)
694 }
695
696 /// Execute a SQL-like query against Vtiger data.
697 ///
698 /// This method allows you to query Vtiger records using a SQL-like syntax.
699 /// It returns multiple records and is the primary method for searching
700 /// and filtering data in Vtiger.
701 ///
702 /// # Arguments
703 ///
704 /// * `query` - SQL-like query string using Vtiger's query syntax
705 ///
706 /// # Returns
707 ///
708 /// Returns a [`VtigerQueryResponse`] containing an array of matching records
709 /// on success, or error details on failure.
710 ///
711 /// # Query Syntax
712 ///
713 /// Vtiger supports a subset of SQL:
714 /// * `SELECT * FROM ModuleName`
715 /// * `SELECT field1, field2 FROM ModuleName`
716 /// * `WHERE` conditions with `=`, `!=`, `LIKE`, `IN`
717 /// * `ORDER BY` for sorting
718 /// * `LIMIT` for pagination (recommended: ≤200 records)
719 ///
720 /// # Examples
721 ///
722 /// ```no_run
723 /// use vtiger_client::Vtiger;
724 ///
725 /// #[tokio::main]
726 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
727 /// let vtiger = Vtiger::new("https://demo.vtiger.com", "admin", "key");
728 ///
729 /// // Basic query
730 /// let response = vtiger.query("SELECT * FROM Leads LIMIT 10").await?;
731 ///
732 /// // Query with conditions
733 /// let response = vtiger.query(
734 /// "SELECT firstname, lastname, email FROM Leads WHERE email != ''"
735 /// ).await?;
736 ///
737 /// // Query with LIKE operator
738 /// let response = vtiger.query(
739 /// "SELECT * FROM Leads WHERE lastname LIKE 'Smith%'"
740 /// ).await?;
741 ///
742 /// // Query with ordering
743 /// let response = vtiger.query(
744 /// "SELECT * FROM Leads ORDER BY createdtime DESC LIMIT 50"
745 /// ).await?;
746 ///
747 /// if response.success {
748 /// if let Some(records) = response.result {
749 /// println!("Found {} records", records.len());
750 /// for record in records {
751 /// println!("Record: {:#?}", record);
752 /// }
753 /// }
754 /// } else {
755 /// eprintln!("Query failed: {:?}", response.error);
756 /// }
757 ///
758 /// Ok(())
759 /// }
760 /// ```
761 ///
762 /// # Performance Considerations
763 ///
764 /// * **Limit results**: Always use `LIMIT` to avoid timeouts
765 /// * **Batch large queries**: Use pagination for large datasets
766 /// * **Index usage**: Filter on indexed fields when possible
767 /// * **Concurrent queries**: Use multiple queries for better performance
768 ///
769 /// # Common Query Patterns
770 ///
771 /// ```sql
772 /// -- Get recent records
773 /// SELECT * FROM Leads ORDER BY createdtime DESC LIMIT 100
774 ///
775 /// -- Filter by custom field
776 /// SELECT * FROM Leads WHERE location LIKE 'Los Angeles%'
777 ///
778 /// -- Count records
779 /// SELECT count(*) FROM Leads WHERE leadstatus = 'Hot'
780 ///
781 /// -- Multiple conditions
782 /// SELECT * FROM Leads WHERE email != '' AND leadstatus IN ('Hot', 'Warm')
783 /// ```
784 ///
785 /// # Errors
786 ///
787 /// This method will return an error if:
788 /// * The network request fails
789 /// * The response cannot be parsed as JSON
790 /// * Invalid SQL syntax is used
791 /// * Referenced fields or modules don't exist
792 /// * Query exceeds time or result limits
793 pub async fn query(&self, query: &str) -> Result<VtigerQueryResponse, reqwest::Error> {
794 let response = self.get(&format!("/query"), &[("query", query)]).await?;
795 let vtiger_response = response.json::<VtigerQueryResponse>().await?;
796 Ok(vtiger_response)
797 }
798}