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}