filemaker_lib/
lib.rs

1//!
2//! # Filemaker Library (filemaker-lib)
3//! 
4//! This project is a Rust library (`filemaker-lib`) designed to interact with the FileMaker Data API. It provides a simple API to perform key operations against FileMaker databases, such as fetching records, performing searches, updating records, deleting records, and manipulating database sessions.
5//! 
6//! ## Features
7//! 
8//! - **[Database Interaction](#fetching-databases)**: Fetch the list of available databases.
9//! - **[Authentication](#initialization)**: Securely manage session tokens for interacting with the FileMaker Data API.
10//! - **[Record Management](#record-management)**:
11//!   - [Fetch records](#fetching-records) (paginated or all at once).
12//!   - [Search for records](#searching-records) based on custom queries and sorting criteria.
13//!   - [Add single or multiple records](#adding-records).
14//!   - [Update specific records](#updating-records).
15//!   - [Delete records](#deleting-records) from a database.
16//! - **[Database Layouts](#fetching-available-layouts)**: Retrieve the layouts available in a given database.
17//! - **[Database Clearing](#clearing-the-database)**: Delete all records within a database.
18//! - **[Advanced Querying and Sorting](#searching-records)**: Search with advanced queries and sort results in ascending or descending order.
19//! - **Utility Functions**: Extract field names from records and encode database parameters safely.
20//! 
21//! ## Installation
22//! 
23//! Add `filemaker-lib` to your project's `Cargo.toml`:
24//! 
25//! ```toml
26//! [dependencies]
27//! filemaker-lib = "0.1.0"
28//! ```
29//! or using git
30//! 
31//! ```toml
32//! [dependencies]
33//! filemaker-lib = {git = "https://github.com/Drew-Chase/filemaker-lib.git"}
34//! ```
35//! 
36//! ## Usage
37//! 
38//! ### Initialization
39//! 
40//! To create a `Filemaker` instance, you need to pass valid credentials (username and password), the name of the database, and the table you want to work with:
41//! 
42//! ```rust
43//! use filemaker_lib::Filemaker;
44//! use anyhow::Result;
45//! 
46//! #[tokio::main]
47//! async fn main() -> Result<()> {
48//!   std::env::set_var("FM_URL", "https://fm.example.com/fmi/data/vLatest"); // Replace with actual FileMaker server URL
49//!   let username = "your_username";
50//!   let password = "your_password";
51//!   let database = "your_database";
52//!   let table = "your_table";
53//! 
54//!   let filemaker = Filemaker::new(username, password, database, table).await?;
55//!   println!("Filemaker instance created successfully.");
56//!   Ok(())
57//! }
58//! ```
59//! 
60//! ### Fetching Records
61//! 
62//! Retrieve specific records with pagination:
63//! 
64//! ```rust
65//! let records = filemaker.get_records(1, 10).await?;
66//! println!("Fetched Records: {:?}", records);
67//! ```
68//! 
69//! Fetch all records at once:
70//! 
71//! ```rust
72//! let all_records = filemaker.get_all_records().await?;
73//! println!("All Records: {:?}", all_records);
74//! ```
75//! 
76//! ### Adding Records
77//! 
78//! #### Adding a Single Record
79//! 
80//! To add a single record to your FileMaker database:
81//! 
82//! ```rust
83//! use serde_json::Value;
84//! use std::collections::HashMap;
85//! 
86//! let mut single_record_data = HashMap::new();
87//! single_record_data.insert("field_name1".to_string(), Value::String("Value 1".to_string()));
88//! single_record_data.insert("field_name2".to_string(), Value::String("Value 2".to_string()));
89//! 
90//! let result = filemaker.add_record(single_record_data).await?;
91//! println!("Single record added: {:?}", result);
92//! ```
93//! 
94//! #### Adding Multiple Records
95//! 
96//! To add multiple records to your FileMaker database:
97//! 
98//! ```rust
99//! use serde_json::Value;
100//! use std::collections::HashMap;
101//! 
102//! let records = vec![
103//!   {
104//!     let mut record = HashMap::new();
105//!     record.insert("field_name1".to_string(), Value::String("First Record - Value 1".to_string()));
106//!     record.insert("field_name2".to_string(), Value::String("First Record - Value 2".to_string()));
107//!     record
108//!   },
109//!   {
110//!     let mut record = HashMap::new();
111//!     record.insert("field_name1".to_string(), Value::String("Second Record - Value 1".to_string()));
112//!     record.insert("field_name2".to_string(), Value::String("Second Record - Value 2".to_string()));
113//!     record
114//!   },
115//! ];
116//! 
117//! for (i, record_data) in records.into_iter().enumerate() {
118//!   match filemaker.add_record(record_data).await {
119//!   Ok(result) => println!("Record {} added successfully: {:?}", i + 1, result),
120//!   Err(e) => eprintln!("Failed to add record {}: {}", i + 1, e),
121//!   }
122//! }
123//! ```
124//! 
125//! ### Counting Records
126//! 
127//! Count the total number of records available in the table:
128//! 
129//! ```rust
130//! let total_records = filemaker.get_number_of_records().await?;
131//! println!("Total Records: {}", total_records);
132//! ```
133//! 
134//! ### Searching Records
135//! 
136//! Perform a query with search parameters and sorting:
137//! 
138//! ```rust
139//! use std::collections::HashMap;
140//! 
141//! let mut query = HashMap::new();
142//! query.insert("fieldName".to_string(), "example_value".to_string());
143//! 
144//! let sort_fields = vec!["fieldName".to_string()];
145//! let ascending = true;
146//! 
147//! let search_results = filemaker.search(vec![query], sort_fields, ascending).await?;
148//! println!("Search Results: {:?}", search_results);
149//! ```
150//! 
151//! ### Updating Records
152//! 
153//! Update a record by its ID:
154//! 
155//! ```rust
156//! use serde_json::Value;
157//! 
158//! let record_id = 123;
159//! let mut field_data = HashMap::new();
160//! field_data.insert("fieldName".to_string(), Value::String("new_value".to_string()));
161//! 
162//! let update_result = filemaker.update_record(record_id, field_data).await?;
163//! println!("Update Result: {:?}", update_result);
164//! ```
165//! 
166//! ### Deleting Records
167//! 
168//! Delete a record by its ID:
169//! 
170//! ```rust
171//! let record_id = 123;
172//! filemaker.delete_record(record_id).await?;
173//! println!("Record deleted successfully.");
174//! ```
175//! 
176//! ### Fetching Available Layouts
177//! 
178//! Retrieve a list of layouts in the specified database:
179//! 
180//! ```rust
181//! let layouts = Filemaker::get_layouts("your_username", "your_password", "your_database").await?;
182//! println!("Available Layouts: {:?}", layouts);
183//! ```
184//! 
185//! ### Fetching Databases
186//! 
187//! Retrieve the list of databases accessible with your credentials:
188//! 
189//! ```rust
190//! let databases = Filemaker::get_databases("your_username", "your_password").await?;
191//! println!("Databases: {:?}", databases);
192//! ```
193//! 
194//! ### Clearing the Database
195//! 
196//! Delete all records from the current database and table:
197//! 
198//! ```rust
199//! filemaker.clear_database().await?;
200//! println!("All records cleared successfully.");
201//! ```
202//! 
203//! ## Environment Variables
204//! 
205//! The library uses the `FM_URL` environment variable to specify the base URL of the FileMaker server. You need to set this variable before using the library:
206//! 
207//! ```rust
208//! std::env::set_var("FM_URL", "https://fm.example.com/fmi/data/vLatest");
209//! ```
210//! 
211//! Replace `"https://fm.example.com/fmi/data/vLatest"` with the actual URL of your FileMaker server.
212//! 
213//! ## Examples
214//! 
215//! This library comes with example implementations usable as references:
216//! 
217//! 1. **Fetch List of Databases**: [`get_databases`](examples/get_databases.rs)
218//! 2. **Fetch Layouts from a Database**: [`filemaker_layout_fetcher`](examples/filemaker_layout_fetcher.rs)
219//! 3. **Retrieve Records**: [`main_filemaker`](examples/main_filemaker.rs)
220//! 4. **Add Single Record**: [`filemaker_single_record_adder`](examples/filemaker_single_record_adder.rs)
221//! 5. **Add Multiple Records**: [`filemaker_multiple_record_adder`](examples/filemaker_multiple_record_adder.rs)
222//! 6. **Update Database Records**: [`filemaker_record_updater`](examples/filemaker_record_updater.rs)
223//! 7. **Delete Database Records**: [`filemaker_record_deleter`](examples/filemaker_record_deleter.rs)
224//! 8. **Find Records Based on Query**: [`filemaker_search_results_output`](examples/filemaker_search_results_output.rs)
225//! 
226//! ## Logging
227//! 
228//! The library uses the [`log`](https://docs.rs/log/) crate for logging. To capture and display logs, set up a logging framework such as [`env_logger`](https://docs.rs/env_logger/). Example:
229//! 
230//! ```rust
231//! use env_logger;
232//! 
233//! fn main() {
234//!   env_logger::init();
235//! }
236//! ```
237//! 
238//! ## License
239//! 
240//! This library is licensed under the terms of the license detailed in the [`LICENSE`](LICENSE) file.
241//! 
242//! ---
243//! 
244//! For more information, please refer to the [repository documentation](https://github.com/Drew-Chase/filemaker-lib). Contributions are welcome!
245
246
247use anyhow::Result;
248use base64::Engine;
249use log::*;
250use reqwest::{Client, Method};
251use serde_json::{json, Value};
252use std::collections::HashMap;
253use std::sync::Arc;
254use tokio::sync::Mutex;
255
256/// Represents a connection to a Filemaker database with authentication and query capabilities.
257///
258/// This struct manages the connection details and authentication token needed
259/// to interact with a Filemaker database through its Data API.
260#[derive(Clone)]
261pub struct Filemaker {
262    // Name of the database to connect to
263    database: String,
264    // Authentication token stored in thread-safe container that can be updated
265    // Option is used since the token might not be available initially
266    token: Arc<Mutex<Option<String>>>,
267    // Name of the table/layout to operate on
268    table: String,
269    // HTTP client for making API requests
270    client: Client,
271}
272impl Filemaker {
273    /// Creates a new `Filemaker` instance.
274    ///
275    /// Initializes a connection to a FileMaker database with the provided credentials.
276    /// This function performs authentication and sets up the HTTP client with appropriate configuration.
277    ///
278    /// # Arguments
279    /// * `username` - The username for FileMaker authentication
280    /// * `password` - The password for FileMaker authentication
281    /// * `database` - The name of the FileMaker database to connect to
282    /// * `table` - The name of the table/layout to operate on
283    ///
284    /// # Returns
285    /// * `Result<Self>` - A new Filemaker instance or an error
286    pub async fn new(username: &str, password: &str, database: &str, table: &str) -> Result<Self> {
287        // URL-encode database and table names to handle spaces and special characters
288        let encoded_database = Self::encode_parameter(database);
289        let encoded_table = Self::encode_parameter(table);
290
291        // Create an HTTP client that accepts invalid SSL certificates (for development)
292        let client = Client::builder()
293            .danger_accept_invalid_certs(true) // Disable SSL verification
294            .build()
295            .map_err(|e| {
296                error!("Failed to build client: {}", e);
297                anyhow::anyhow!(e)
298            })?;
299
300        // Authenticate with FileMaker and obtain a session token
301        let token = Self::get_session_token(&client, database, username, password).await?;
302        info!("Filemaker instance created successfully");
303
304        // Return the initialized Filemaker instance
305        Ok(Self {
306            database: encoded_database,
307            table: encoded_table,
308            token: Arc::new(Mutex::new(Some(token))), // Wrap token in thread-safe container
309            client,
310        })
311    }
312
313    /// Gets a session token from the FileMaker Data API.
314    ///
315    /// Performs authentication against the FileMaker Data API and retrieves a session token
316    /// that can be used for subsequent API requests.
317    ///
318    /// # Arguments
319    /// * `client` - The HTTP client to use for the request
320    /// * `database` - The name of the FileMaker database to authenticate against
321    /// * `username` - The username for FileMaker authentication
322    /// * `password` - The password for FileMaker authentication
323    ///
324    /// # Returns
325    /// * `Result<String>` - The session token or an error
326    async fn get_session_token(
327        client: &Client,
328        database: &str,
329        username: &str,
330        password: &str,
331    ) -> Result<String> {
332        // URL-encode the database name to handle spaces and special characters
333        let database = Self::encode_parameter(database);
334
335        // Construct the URL for the sessions endpoint
336        let url = format!(
337            "{}/databases/{}/sessions",
338            std::env::var("FM_URL").unwrap_or_default().as_str(),
339            database
340        );
341
342        // Create a Base64-encoded Basic authentication header
343        let auth_header = format!(
344            "Basic {}",
345            base64::engine::general_purpose::STANDARD.encode(format!("{}:{}", username, password))
346        );
347
348        debug!("Requesting session token from URL: {}", url);
349
350        // Send the authentication request to FileMaker
351        let response = client
352            .post(&url)
353            .header("Authorization", auth_header)
354            .header("Content-Type", "application/json")
355            .body("{}") // Empty JSON body for session creation
356            .send()
357            .await
358            .map_err(|e| {
359                error!("Failed to send request for session token: {}", e);
360                anyhow::anyhow!(e)
361            })?;
362
363        // Parse the JSON response
364        let json: Value = response.json().await.map_err(|e| {
365            error!("Failed to parse session token response: {}", e);
366            anyhow::anyhow!(e)
367        })?;
368
369        // Extract the token from the response JSON structure
370        if let Some(token) = json
371            .get("response")
372            .and_then(|r| r.get("token"))
373            .and_then(|t| t.as_str())
374        {
375            info!("Session token retrieved successfully");
376            Ok(token.to_string())
377        } else {
378            error!(
379                "Failed to get token from FileMaker API response: {:?}",
380                json
381            );
382            Err(anyhow::anyhow!("Failed to get token from FileMaker API"))
383        }
384    }
385
386    /// Sends an authenticated HTTP request to the FileMaker Data API.
387    ///
388    /// This method handles adding the authentication token to requests and processing
389    /// the response from the FileMaker Data API.
390    ///
391    /// # Arguments
392    /// * `url` - The endpoint URL to send the request to
393    /// * `method` - The HTTP method to use (GET, POST, etc.)
394    /// * `body` - Optional JSON body to include with the request
395    ///
396    /// # Returns
397    /// * `Result<Value>` - The parsed JSON response or an error
398    async fn authenticated_request(
399        &self,
400        url: &str,
401        method: Method,
402        body: Option<Value>,
403    ) -> Result<Value> {
404        // Retrieve the session token from the shared state
405        let token = self.token.lock().await.clone();
406        if token.is_none() {
407            error!("No session token found");
408            return Err(anyhow::anyhow!("No session token found"));
409        }
410
411        // Create Bearer authentication header with the token
412        let auth_header = format!("Bearer {}", token.unwrap());
413
414        // Start building the request with appropriate headers
415        let mut request = self
416            .client
417            .request(method, url)
418            .header("Authorization", auth_header)
419            .header("Content-Type", "application/json");
420
421        // Add the JSON body to the request if provided
422        if let Some(body_content) = body {
423            let json_body = serde_json::to_string(&body_content).map_err(|e| {
424                error!("Failed to serialize request body: {}", e);
425                anyhow::anyhow!(e)
426            })?;
427            debug!("Request body: {}", json_body);
428            request = request.body(json_body);
429        }
430
431        debug!("Sending authenticated request to URL: {}", url);
432
433        // Send the request and handle any network errors
434        let response = request.send().await.map_err(|e| {
435            error!("Failed to send authenticated request: {}", e);
436            anyhow::anyhow!(e)
437        })?;
438
439        // Parse the response JSON and handle parsing errors
440        let json: Value = response.json().await.map_err(|e| {
441            error!("Failed to parse authenticated request response: {}", e);
442            anyhow::anyhow!(e)
443        })?;
444
445        info!("Authenticated request to {} completed successfully", url);
446        Ok(json)
447    }
448
449    /// Retrieves a specified range of records from the database.
450    ///
451    /// # Arguments
452    /// * `start` - The starting position (offset) for record retrieval
453    /// * `limit` - The maximum number of records to retrieve
454    ///
455    /// # Returns
456    /// * `Result<Vec<Value>>` - A vector of record objects on success, or an error
457    pub async fn get_records<T>(&self, start: T, limit: T) -> Result<Vec<Value>>
458    where
459        T: Sized + Clone + std::fmt::Display + std::str::FromStr + TryFrom<usize>,
460    {
461        // Construct the URL for the FileMaker Data API records endpoint
462        let url = format!(
463            "{}/databases/{}/layouts/{}/records?_offset={}&_limit={}",
464            std::env::var("FM_URL").unwrap_or_default().as_str(),
465            self.database,
466            self.table,
467            start,
468            limit
469        );
470        debug!("Fetching records from URL: {}", url);
471
472        // Send authenticated request to the API endpoint
473        let response = self.authenticated_request(&url, Method::GET, None).await?;
474
475        // Extract the records data from the response if available
476        if let Some(data) = response.get("response").and_then(|r| r.get("data")) {
477            info!("Successfully retrieved records from database");
478            Ok(data.as_array().unwrap_or(&vec![]).clone())
479        } else {
480            // Log and return error if the expected data structure is not found
481            error!("Failed to retrieve records from response: {:?}", response);
482            Err(anyhow::anyhow!("Failed to retrieve records"))
483        }
484    }
485
486    /// Retrieves all records from the database in a single query.
487    ///
488    /// This method first determines the total record count and then
489    /// fetches all records in a single request.
490    ///
491    /// # Returns
492    /// * `Result<Vec<Value>>` - A vector containing all records on success, or an error
493    pub async fn get_all_records(&self) -> Result<Vec<Value>> {
494        // First get the total number of records in the database
495        let total_count = self.get_number_of_records().await?;
496        debug!("Total records to fetch: {}", total_count);
497
498        // Retrieve all records in a single request
499        self.get_records(1, total_count).await
500    }
501
502    /// Retrieves the total number of records in the database table.
503    ///
504    /// # Returns
505    /// * `Result<u64>` - The total record count on success, or an error
506    pub async fn get_number_of_records(&self) -> Result<u64> {
507        // Construct the URL for the FileMaker Data API records endpoint
508        let url = format!(
509            "{}/databases/{}/layouts/{}/records",
510            std::env::var("FM_URL").unwrap_or_default().as_str(),
511            self.database,
512            self.table
513        );
514        debug!("Fetching total number of records from URL: {}", url);
515
516        // Send authenticated request to the API endpoint
517        let response = self.authenticated_request(&url, Method::GET, None).await?;
518
519        // Extract the total record count from the response if available
520        if let Some(total_count) = response
521            .get("response")
522            .and_then(|r| r.get("dataInfo"))
523            .and_then(|d| d.get("totalRecordCount"))
524            .and_then(|c| c.as_u64())
525        {
526            info!("Total record count retrieved successfully: {}", total_count);
527            Ok(total_count)
528        } else {
529            // Log and return error if the expected data structure is not found
530            error!(
531                "Failed to retrieve total record count from response: {:?}",
532                response
533            );
534            Err(anyhow::anyhow!("Failed to retrieve total record count"))
535        }
536    }
537
538    /// Searches the database for records matching specified criteria.
539    ///
540    /// # Arguments
541    /// * `query` - Vector of field-value pairs to search for
542    /// * `sort` - Vector of field names to sort by
543    /// * `ascending` - Whether to sort in ascending (true) or descending (false) order
544    ///
545    /// # Returns
546    /// * `Result<Vec<Value>>` - A vector of matching records on success, or an error
547    pub async fn search(
548        &self,
549        query: Vec<HashMap<String, String>>,
550        sort: Vec<String>,
551        ascending: bool,
552    ) -> Result<Vec<Value>> {
553        // Construct the URL for the FileMaker Data API find endpoint
554        let url = format!(
555            "{}/databases/{}/layouts/{}/_find",
556            std::env::var("FM_URL").unwrap_or_default().as_str(),
557            self.database,
558            self.table
559        );
560
561        // Determine sort order based on ascending parameter
562        let sort_order = if ascending { "ascend" } else { "descend" };
563
564        // Transform the sort fields into the format expected by FileMaker API
565        let sort_map: Vec<_> = sort
566            .into_iter()
567            .map(|s| {
568                let mut map = HashMap::new();
569                map.insert("fieldName".to_string(), s);
570                map.insert("sortOrder".to_string(), sort_order.to_string());
571                map
572            })
573            .collect();
574
575        // Construct the request body with query and sort parameters
576        let body: HashMap<String, Value> = HashMap::from([
577            ("query".to_string(), serde_json::to_value(query)?),
578            ("sort".to_string(), serde_json::to_value(sort_map)?),
579        ]);
580        debug!("Executing search query with URL: {}. Body: {:?}", url, body);
581
582        // Send authenticated POST request to the API endpoint
583        let response = self
584            .authenticated_request(&url, Method::POST, Some(serde_json::to_value(body)?))
585            .await?;
586
587        // Extract the search results from the response if available
588        if let Some(data) = response.get("response").and_then(|r| r.get("data")) {
589            info!("Search query executed successfully");
590            Ok(data.as_array().unwrap_or(&vec![]).clone())
591        } else {
592            // Log and return error if the expected data structure is not found
593            error!(
594                "Failed to retrieve search results from response: {:?}",
595                response
596            );
597            Err(anyhow::anyhow!("Failed to retrieve search results"))
598        }
599    }
600
601    /// Adds a record to the database.
602    ///
603    /// # Parameters
604    /// - `field_data`: A `HashMap` representing the field data for the new record.
605    ///
606    /// # Returns
607    /// A `Result` containing the added record as a `Value` on success, or an error.
608    pub async fn add_record(
609        &self,
610        field_data: HashMap<String, Value>,
611    ) -> Result<HashMap<String, Value>> {
612        // Define the URL for the FileMaker Data API endpoint
613        let url = format!(
614            "{}/databases/{}/layouts/{}/records",
615            std::env::var("FM_URL").unwrap_or_default().as_str(),
616            self.database,
617            self.table
618        );
619
620        // Prepare the request body
621        let field_data_map: serde_json::Map<String, Value> = field_data.into_iter().collect();
622        let body = HashMap::from([("fieldData".to_string(), Value::Object(field_data_map))]);
623
624        debug!("Adding a new record. URL: {}. Body: {:?}", url, body);
625
626        // Make the API call
627        let response = self
628            .authenticated_request(&url, Method::POST, Some(serde_json::to_value(body)?))
629            .await?;
630
631        if let Some(record_id) = response
632            .get("response")
633            .and_then(|r| r.get("recordId"))
634            .and_then(|id| id.as_str())
635        {
636            if let Ok(record_id) = record_id.parse::<u64>() {
637                debug!("Record added successfully. Record ID: {}", record_id);
638                let added_record = self.get_record_by_id(record_id).await?;
639                Ok(HashMap::from([
640                    ("success".to_string(), Value::Bool(true)),
641                    ("result".to_string(), added_record),
642                ]))
643            } else {
644                error!("Failed to parse record id {} - {:?}", record_id, response);
645                Ok(HashMap::from([
646                    ("success".to_string(), Value::Bool(false)),
647                    ("result".to_string(), response),
648                ]))
649            }
650        } else {
651            error!("Failed to add the record: {:?}", response);
652            Ok(HashMap::from([
653                ("success".to_string(), Value::Bool(false)),
654                ("result".to_string(), response),
655            ]))
656        }
657    }
658
659    /// Updates a record in the database using the FileMaker Data API.
660    ///
661    /// # Arguments
662    /// * `id` - The unique identifier of the record to update
663    /// * `field_data` - A hashmap containing the field names and their new values
664    ///
665    /// # Returns
666    /// * `Result<Value>` - The server response as a JSON value or an error
667    ///
668    /// # Type Parameters
669    /// * `T` - A type that can be used as a record identifier and meets various trait requirements
670    pub async fn update_record<T>(&self, id: T, field_data: HashMap<String, Value>) -> Result<Value>
671    where
672        T: Sized + Clone + std::fmt::Display + std::str::FromStr + TryFrom<usize>,
673    {
674        // Construct the API endpoint URL for updating a specific record
675        let url = format!(
676            "{}/databases/{}/layouts/{}/records/{}",
677            std::env::var("FM_URL").unwrap_or_default().as_str(),
678            self.database,
679            self.table,
680            id
681        );
682
683        // Convert the field data hashmap to the format expected by FileMaker Data API
684        let field_data_map: serde_json::Map<String, Value> = field_data.into_iter().collect();
685        // Create the request body with fieldData property
686        let body = HashMap::from([("fieldData".to_string(), Value::Object(field_data_map))]);
687
688        debug!("Updating record ID: {}. URL: {}. Body: {:?}", id, url, body);
689
690        // Send the PATCH request to update the record
691        let response = self
692            .authenticated_request(&url, Method::PATCH, Some(serde_json::to_value(body)?))
693            .await?;
694
695        info!("Record ID: {} updated successfully", id);
696        Ok(response)
697    }
698
699    /// Retrieves the list of databases accessible to the specified user.
700    ///
701    /// # Arguments
702    /// * `username` - The FileMaker username for authentication
703    /// * `password` - The FileMaker password for authentication
704    ///
705    /// # Returns
706    /// * `Result<Vec<String>>` - A list of accessible database names or an error
707    pub async fn get_databases(username: &str, password: &str) -> Result<Vec<String>> {
708        // Construct the API endpoint URL for retrieving databases
709        let url = format!(
710            "{}/databases",
711            std::env::var("FM_URL").unwrap_or_default().as_str()
712        );
713
714        // Create Base64 encoded Basic auth header from username and password
715        let auth_header = format!(
716            "Basic {}",
717            base64::engine::general_purpose::STANDARD.encode(format!("{}:{}", username, password))
718        );
719
720        debug!("Fetching list of databases from URL: {}", url);
721
722        // Initialize HTTP client
723        let client = Client::new();
724
725        // Send request to get list of databases with authentication
726        let response = client
727            .get(&url)
728            .header("Authorization", auth_header)
729            .header("Content-Type", "application/json")
730            .send()
731            .await
732            .map_err(|e| {
733                error!("Failed to send request for databases: {}", e);
734                anyhow::anyhow!(e)
735            })?
736            .json::<Value>()
737            .await
738            .map_err(|e| {
739                error!("Failed to parse database list response: {}", e);
740                anyhow::anyhow!(e)
741            })?;
742
743        // Extract database names from the response JSON
744        if let Some(databases) = response
745            .get("response")
746            .and_then(|r| r.get("databases"))
747            .and_then(|d| d.as_array())
748        {
749            // Extract the name field from each database object
750            let database_names = databases
751                .iter()
752                .filter_map(|db| {
753                    db.get("name")
754                        .and_then(|n| n.as_str())
755                        .map(|s| s.to_string())
756                })
757                .collect();
758
759            info!("Database list retrieved successfully");
760            Ok(database_names)
761        } else {
762            // Handle case where response doesn't contain expected data structure
763            error!("Failed to retrieve databases from response: {:?}", response);
764            Err(anyhow::anyhow!("Failed to retrieve databases"))
765        }
766    }
767
768    /// Retrieves the list of layouts for the specified database using the provided credentials.
769    ///
770    /// # Arguments
771    /// * `username` - The FileMaker username for authentication
772    /// * `password` - The FileMaker password for authentication
773    /// * `database` - The name of the database to get layouts from
774    ///
775    /// # Returns
776    /// * `Result<Vec<String>>` - A list of layout names or an error
777    pub async fn get_layouts(
778        username: &str,
779        password: &str,
780        database: &str,
781    ) -> Result<Vec<String>> {
782        // URL encode the database name and construct the API endpoint URL
783        let encoded_database = Self::encode_parameter(database);
784        let url = format!(
785            "{}/databases/{}/layouts",
786            std::env::var("FM_URL").unwrap_or_default().as_str(),
787            encoded_database
788        );
789
790        debug!("Fetching layouts from URL: {}", url);
791
792        // Create HTTP client and get session token for authentication
793        let client = Client::new();
794        let token = Self::get_session_token(&client, database, username, password)
795            .await
796            .map_err(|e| {
797                error!("Failed to get session token for layouts: {}", e);
798                anyhow::anyhow!(e)
799            })?;
800
801        // Create Bearer auth header from the session token
802        let auth_header = format!("Bearer {}", token);
803
804        // Send request to get list of layouts with token authentication
805        let response = client
806            .get(&url)
807            .header("Authorization", auth_header)
808            .header("Content-Type", "application/json")
809            .send()
810            .await
811            .map_err(|e| {
812                error!("Failed to send request to retrieve layouts: {}", e);
813                anyhow::anyhow!(e)
814            })?
815            .json::<Value>()
816            .await
817            .map_err(|e| {
818                error!("Failed to parse response for layouts: {}", e);
819                anyhow::anyhow!(e)
820            })?;
821
822        // Extract layout names from the response JSON
823        if let Some(layouts) = response
824            .get("response")
825            .and_then(|r| r.get("layouts"))
826            .and_then(|l| l.as_array())
827        {
828            // Extract the name field from each layout object
829            let layout_names = layouts
830                .iter()
831                .filter_map(|layout| {
832                    layout
833                        .get("name")
834                        .and_then(|n| n.as_str())
835                        .map(|s| s.to_string())
836                })
837                .collect();
838
839            info!("Successfully retrieved layouts");
840            Ok(layout_names)
841        } else {
842            // Handle case where response doesn't contain expected data structure
843            error!("Failed to retrieve layouts from response: {:?}", response);
844            Err(anyhow::anyhow!("Failed to retrieve layouts"))
845        }
846    }
847
848    /// Gets a record from the database by its ID.
849    ///
850    /// # Arguments
851    /// * `id` - The ID of the record to get.
852    ///
853    /// # Returns
854    /// A JSON object representing the record.
855    pub async fn get_record_by_id<T>(&self, id: T) -> Result<Value>
856    where
857        T: Sized + Clone + std::fmt::Display + std::str::FromStr + TryFrom<usize>,
858    {
859        let url = format!(
860            "{}/databases/{}/layouts/{}/records/{}",
861            std::env::var("FM_URL").unwrap_or_default().as_str(),
862            self.database,
863            self.table,
864            id
865        );
866
867        debug!("Fetching record with ID: {} from URL: {}", id, url);
868
869        let response = self
870            .authenticated_request(&url, Method::GET, None)
871            .await
872            .map_err(|e| {
873                error!("Failed to get record ID {}: {}", id, e);
874                anyhow::anyhow!(e)
875            })?;
876
877        if let Some(data) = response.get("response").and_then(|r| r.get("data")) {
878            if let Some(record) = data.as_array().and_then(|arr| arr.first()) {
879                info!("Record ID {} retrieved successfully", id);
880                Ok(record.clone())
881            } else {
882                error!("No record found for ID {}", id);
883                Err(anyhow::anyhow!("No record found"))
884            }
885        } else {
886            error!("Failed to get record from response: {:?}", response);
887            Err(anyhow::anyhow!("Failed to get record"))
888        }
889    }
890
891    /// Deletes a record from the database by its ID.
892    ///
893    /// # Arguments
894    /// * `id` - The ID of the record to delete.
895    ///
896    /// # Returns
897    /// A result indicating the deletion was successful or an error message.
898    pub async fn delete_record<T>(&self, id: T) -> Result<Value>
899    where
900        T: Sized + Clone + std::fmt::Display + std::str::FromStr + TryFrom<usize>,
901    {
902        let url = format!(
903            "{}/databases/{}/layouts/{}/records/{}",
904            std::env::var("FM_URL").unwrap_or_default().as_str(),
905            self.database,
906            self.table,
907            id
908        );
909
910        debug!("Deleting record with ID: {} at URL: {}", id, url);
911
912        let response = self
913            .authenticated_request(&url, Method::DELETE, None)
914            .await
915            .map_err(|e| {
916                error!("Failed to delete record ID {}: {}", id, e);
917                anyhow::anyhow!(e)
918            })?;
919
920        if response.is_object() {
921            info!("Record ID {} deleted successfully", id);
922            Ok(json!({"success": true}))
923        } else {
924            error!("Failed to delete record ID {}", id);
925            Err(anyhow::anyhow!("Failed to delete record"))
926        }
927    }
928
929    /// Deletes the specified database.
930    ///
931    /// # Arguments
932    /// * `database` - The name of the database to delete.
933    /// * `username` - The username for authentication.
934    /// * `password` - The password for authentication.
935    pub async fn delete_database(database: &str, username: &str, password: &str) -> Result<()> {
936        let encoded_database = Self::encode_parameter(database);
937        let url = format!(
938            "{}/databases/{}",
939            std::env::var("FM_URL").unwrap_or_default().as_str(),
940            encoded_database
941        );
942
943        debug!("Deleting database: {}", database);
944
945        let client = Client::new();
946        let token = Self::get_session_token(&client, database, username, password)
947            .await
948            .map_err(|e| {
949                error!("Failed to get session token for database deletion: {}", e);
950                anyhow::anyhow!(e)
951            })?;
952        let auth_header = format!("Bearer {}", token);
953
954        client
955            .delete(&url)
956            .header("Authorization", auth_header)
957            .header("Content-Type", "application/json")
958            .send()
959            .await
960            .map_err(|e| {
961                error!("Failed to delete database {}: {}", database, e);
962                anyhow::anyhow!(e)
963            })?;
964
965        info!("Database {} deleted successfully", database);
966        Ok(())
967    }
968
969    /// Deletes all records from the current database.
970    ///
971    /// This function retrieves and systematically removes all records from the database.
972    /// It first checks if there are any records to delete, then proceeds with deletion
973    /// if records exist.
974    ///
975    /// # Returns
976    /// * `Result<()>` - Ok(()) if all records were successfully deleted, or an error
977    ///
978    /// # Errors
979    /// * Returns error if unable to retrieve records
980    /// * Returns error if record ID parsing fails
981    /// * Returns error if record deletion fails
982    pub async fn clear_database(&self) -> Result<()> {
983        debug!("Clearing all records from the database");
984        // Get the total count of records in the database
985        let number_of_records = self.get_number_of_records().await?;
986
987        // Check if there are any records to delete
988        if number_of_records == 0 {
989            warn!("No records found in the database. Nothing to clear");
990            return Ok(());
991        }
992
993        // Retrieve all records that need to be deleted
994        // The number_of_records value is used as limit to fetch all records at once
995        let records = self.get_records(1, number_of_records).await.map_err(|e| {
996            error!("Failed to retrieve records for clearing database: {}", e);
997            anyhow::anyhow!(e)
998        })?;
999
1000        // Iterate through each record and delete it individually
1001        for record in records {
1002            // Extract the record ID from the record data
1003            if let Some(id) = record.get("recordId").and_then(|id| id.as_str()) {
1004                // The record ID is usually marked as a string even though it's a u64,
1005                // so we need to parse it to the correct type
1006                if let Ok(id) = id.parse::<u64>() {
1007                    debug!("Deleting record ID: {}", id);
1008                    // Attempt to delete the record and handle any errors
1009                    if let Err(e) = self.delete_record(id).await {
1010                        error!("Failed to delete record ID {}: {}", id, e);
1011                        return Err(anyhow::anyhow!(e));
1012                    }
1013                } else {
1014                    // Handle case where ID exists but cannot be parsed as u64
1015                    error!("Failed to parse record ID {} as u64", id);
1016                    return Err(anyhow::anyhow!("Failed to parse record ID as u64"));
1017                }
1018            } else {
1019                // Handle case where record doesn't contain an ID field
1020                error!("Record ID not found in record: {:?}", record);
1021                return Err(anyhow::anyhow!(
1022                    "Record ID not found in record: {:?}",
1023                    record
1024                ));
1025            }
1026        }
1027
1028        info!("All records cleared from the database");
1029        Ok(())
1030    }
1031    /// Returns the names of fields in the given record excluding the ones starting with 'g_' (global fields)
1032    ///
1033    /// # Arguments
1034    /// * `record` - An example record with 'fieldData' element containing field names as keys.
1035    ///
1036    /// # Returns
1037    /// An array of field names.
1038    pub fn get_row_names_by_example(record: &Value) -> Vec<String> {
1039        let mut fields = Vec::new();
1040        if let Some(field_data) = record.get("fieldData").and_then(|fd| fd.as_object()) {
1041            for field in field_data.keys() {
1042                if !field.starts_with("g_") {
1043                    fields.push(field.clone());
1044                }
1045            }
1046        }
1047        info!("Extracted row names: {:?}", fields);
1048        fields
1049    }
1050
1051    /// Gets the field names for the first record in the database.
1052    ///
1053    /// This function retrieves a single record from the database and extracts
1054    /// field names from it. If no records exist, an empty vector is returned.
1055    ///
1056    /// # Returns
1057    /// * `Result<Vec<String>>` - A vector of field names on success, or an error
1058    pub async fn get_row_names(&self) -> Result<Vec<String>> {
1059        debug!("Attempting to fetch field names for the first record");
1060
1061        // Fetch just the first record to use as a template
1062        let records = self.get_records(1, 1).await?;
1063
1064        if let Some(first_record) = records.first() {
1065            info!("Successfully fetched field names for the first record");
1066            // Extract field names from the first record using the helper method
1067            return Ok(Self::get_row_names_by_example(first_record));
1068        }
1069
1070        // Handle the case where no records exist in the database
1071        warn!("No records found while fetching field names");
1072        Ok(vec![])
1073    }
1074
1075    /// Searches the database for records matching the specified query.
1076    ///
1077    /// # Arguments
1078    /// * `fields` - The query fields.
1079    /// * `sort` - The sort order.
1080    /// * `ascending` - Whether to sort in ascending order.
1081    ///
1082    /// # Returns
1083    /// A vector of matching records.
1084    pub async fn advanced_search(
1085        &self,
1086        fields: HashMap<String, Value>,
1087        sort: Vec<String>,
1088        ascending: bool,
1089    ) -> Result<Vec<Value>> {
1090        let url = format!(
1091            "{}/databases/{}/layouts/{}/_find",
1092            std::env::var("FM_URL").unwrap_or_default().as_str(),
1093            self.database,
1094            self.table
1095        );
1096
1097        debug!(
1098            "Preparing advanced search with fields: {:?}, sort: {:?}, ascending: {}",
1099            fields, sort, ascending
1100        );
1101
1102        let mut content = serde_json::Map::new();
1103        content.insert(
1104            "query".to_string(),
1105            Value::Array(fields.into_iter().map(|(k, v)| json!({ k: v })).collect()),
1106        );
1107
1108        if !sort.is_empty() {
1109            let sort_array: Vec<Value> = sort
1110                .into_iter()
1111                .map(|s| {
1112                    json!({
1113                        "fieldName": s,
1114                        "sortOrder": if ascending { "ascend" } else { "descend" }
1115                    })
1116                })
1117                .collect();
1118            content.insert("sort".to_string(), Value::Array(sort_array));
1119        }
1120
1121        debug!(
1122            "Sending authenticated request to URL: {} with content: {:?}",
1123            url, content
1124        );
1125
1126        let response = self
1127            .authenticated_request(&url, Method::POST, Some(Value::Object(content)))
1128            .await?;
1129
1130        if let Some(data) = response
1131            .get("response")
1132            .and_then(|r| r.get("data"))
1133            .and_then(|d| d.as_array())
1134        {
1135            info!(
1136                "Advanced search completed successfully, retrieved {} records",
1137                data.len()
1138            );
1139            Ok(data.clone())
1140        } else {
1141            error!("Failed to retrieve advanced search results: {:?}", response);
1142            Err(anyhow::anyhow!(
1143                "Failed to retrieve advanced search results"
1144            ))
1145        }
1146    }
1147
1148    /// Encodes a parameter by replacing spaces with `%20`.
1149    ///
1150    /// This function takes a string parameter and replaces all spaces with URL-encoded
1151    /// representation (%20), which is useful for preparing strings to be included in URLs.
1152    ///
1153    /// # Arguments
1154    ///
1155    /// * `parameter` - The string to be encoded
1156    ///
1157    /// # Returns
1158    ///
1159    /// A new String with all spaces replaced by %20
1160    fn encode_parameter(parameter: &str) -> String {
1161        // Replace all spaces with %20 URL encoding
1162        let encoded = parameter.replace(" ", "%20");
1163
1164        // Log the encoding operation at debug level
1165        debug!("Encoded parameter '{}' to '{}'", parameter, encoded);
1166
1167        // Return the encoded string
1168        encoded
1169    }
1170}