filemaker_lib/
lib.rs

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