Skip to main content

filemaker_lib/
lib.rs

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