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::new();
696
697        // Send request to get list of databases with authentication
698        let response = client
699            .get(&url)
700            .header("Authorization", auth_header)
701            .header("Content-Type", "application/json")
702            .send()
703            .await
704            .map_err(|e| {
705                error!("Failed to send request for databases: {}", e);
706                anyhow::anyhow!(e)
707            })?
708            .json::<Value>()
709            .await
710            .map_err(|e| {
711                error!("Failed to parse database list response: {}", e);
712                anyhow::anyhow!(e)
713            })?;
714
715        // Extract database names from the response JSON
716        if let Some(databases) = response
717            .get("response")
718            .and_then(|r| r.get("databases"))
719            .and_then(|d| d.as_array())
720        {
721            // Extract the name field from each database object
722            let database_names = databases
723                .iter()
724                .filter_map(|db| {
725                    db.get("name")
726                        .and_then(|n| n.as_str())
727                        .map(|s| s.to_string())
728                })
729                .collect();
730
731            info!("Database list retrieved successfully");
732            Ok(database_names)
733        } else {
734            // Handle case where response doesn't contain expected data structure
735            error!("Failed to retrieve databases from response: {:?}", response);
736            Err(anyhow::anyhow!("Failed to retrieve databases"))
737        }
738    }
739
740    /// Retrieves the list of layouts for the specified database using the provided credentials.
741    ///
742    /// # Arguments
743    /// * `username` - The FileMaker username for authentication
744    /// * `password` - The FileMaker password for authentication
745    /// * `database` - The name of the database to get layouts from
746    ///
747    /// # Returns
748    /// * `Result<Vec<String>>` - A list of layout names or an error
749    pub async fn get_layouts(
750        username: &str,
751        password: &str,
752        database: &str,
753    ) -> Result<Vec<String>> {
754        // URL encode the database name and construct the API endpoint URL
755        let encoded_database = utf8_percent_encode(database, NON_ALPHANUMERIC).to_string();
756        let url = format!(
757            "{}/databases/{}/layouts",
758            Self::get_fm_url()?,
759            encoded_database
760        );
761
762        debug!("Fetching layouts from URL: {}", url);
763
764        // Create HTTP client and get session token for authentication
765        let client = Client::new();
766        let token = Self::get_session_token(&client, database, username, password)
767            .await
768            .map_err(|e| {
769                error!("Failed to get session token for layouts: {}", e);
770                anyhow::anyhow!(e)
771            })?;
772
773        // Create Bearer auth header from the session token
774        let auth_header = format!("Bearer {}", token);
775
776        // Send request to get list of layouts with token authentication
777        let response = client
778            .get(&url)
779            .header("Authorization", auth_header)
780            .header("Content-Type", "application/json")
781            .send()
782            .await
783            .map_err(|e| {
784                error!("Failed to send request to retrieve layouts: {}", e);
785                anyhow::anyhow!(e)
786            })?
787            .json::<Value>()
788            .await
789            .map_err(|e| {
790                error!("Failed to parse response for layouts: {}", e);
791                anyhow::anyhow!(e)
792            })?;
793
794        // Extract layout names from the response JSON
795        if let Some(layouts) = response
796            .get("response")
797            .and_then(|r| r.get("layouts"))
798            .and_then(|l| l.as_array())
799        {
800            // Extract the name field from each layout object
801            let layout_names = layouts
802                .iter()
803                .filter_map(|layout| {
804                    layout
805                        .get("name")
806                        .and_then(|n| n.as_str())
807                        .map(|s| s.to_string())
808                })
809                .collect();
810
811            info!("Successfully retrieved layouts");
812            Ok(layout_names)
813        } else {
814            // Handle case where response doesn't contain expected data structure
815            error!("Failed to retrieve layouts from response: {:?}", response);
816            Err(anyhow::anyhow!("Failed to retrieve layouts"))
817        }
818    }
819
820    /// Gets a record from the database by its ID.
821    ///
822    /// # Arguments
823    /// * `id` - The ID of the record to get.
824    ///
825    /// # Returns
826    /// A JSON object representing the record.
827    pub async fn get_record_by_id<T>(&self, id: T) -> Result<Value>
828    where
829        T: Sized + Clone + std::fmt::Display + std::str::FromStr + TryFrom<usize>,
830    {
831        let url = format!(
832            "{}/databases/{}/layouts/{}/records/{}",
833            Self::get_fm_url()?,
834            self.database,
835            self.table,
836            id
837        );
838
839        debug!("Fetching record with ID: {} from URL: {}", id, url);
840
841        let response = self
842            .authenticated_request(&url, Method::GET, None)
843            .await
844            .map_err(|e| {
845                error!("Failed to get record ID {}: {}", id, e);
846                anyhow::anyhow!(e)
847            })?;
848
849        if let Some(data) = response.get("response").and_then(|r| r.get("data")) {
850            if let Some(record) = data.as_array().and_then(|arr| arr.first()) {
851                info!("Record ID {} retrieved successfully", id);
852                Ok(record.clone())
853            } else {
854                error!("No record found for ID {}", id);
855                Err(anyhow::anyhow!("No record found"))
856            }
857        } else {
858            error!("Failed to get record from response: {:?}", response);
859            Err(anyhow::anyhow!("Failed to get record"))
860        }
861    }
862
863    /// Deletes a record from the database by its ID.
864    ///
865    /// # Arguments
866    /// * `id` - The ID of the record to delete.
867    ///
868    /// # Returns
869    /// A result indicating the deletion was successful or an error message.
870    pub async fn delete_record<T>(&self, id: T) -> Result<Value>
871    where
872        T: Sized + Clone + std::fmt::Display + std::str::FromStr + TryFrom<usize>,
873    {
874        let url = format!(
875            "{}/databases/{}/layouts/{}/records/{}",
876            Self::get_fm_url()?,
877            self.database,
878            self.table,
879            id
880        );
881
882        debug!("Deleting record with ID: {} at URL: {}", id, url);
883
884        let response = self
885            .authenticated_request(&url, Method::DELETE, None)
886            .await
887            .map_err(|e| {
888                error!("Failed to delete record ID {}: {}", id, e);
889                anyhow::anyhow!(e)
890            })?;
891
892        if response.is_object() {
893            info!("Record ID {} deleted successfully", id);
894            Ok(json!({"success": true}))
895        } else {
896            error!("Failed to delete record ID {}", id);
897            Err(anyhow::anyhow!("Failed to delete record"))
898        }
899    }
900
901    /// Deletes the specified database.
902    ///
903    /// # Arguments
904    /// * `database` - The name of the database to delete.
905    /// * `username` - The username for authentication.
906    /// * `password` - The password for authentication.
907    pub async fn delete_database(database: &str, username: &str, password: &str) -> Result<()> {
908        let encoded_database = utf8_percent_encode(database, NON_ALPHANUMERIC).to_string();
909        let url = format!("{}/databases/{}", Self::get_fm_url()?, encoded_database);
910
911        debug!("Deleting database: {}", database);
912
913        let client = Client::new();
914        let token = Self::get_session_token(&client, database, username, password)
915            .await
916            .map_err(|e| {
917                error!("Failed to get session token for database deletion: {}", e);
918                anyhow::anyhow!(e)
919            })?;
920        let auth_header = format!("Bearer {}", token);
921
922        client
923            .delete(&url)
924            .header("Authorization", auth_header)
925            .header("Content-Type", "application/json")
926            .send()
927            .await
928            .map_err(|e| {
929                error!("Failed to delete database {}: {}", database, e);
930                anyhow::anyhow!(e)
931            })?;
932
933        info!("Database {} deleted successfully", database);
934        Ok(())
935    }
936
937    /// Deletes all records from the current database.
938    ///
939    /// This function retrieves and systematically removes all records from the database.
940    /// It first checks if there are any records to delete, then proceeds with deletion
941    /// if records exist.
942    ///
943    /// # Returns
944    /// * `Result<()>` - Ok(()) if all records were successfully deleted, or an error
945    ///
946    /// # Errors
947    /// * Returns error if unable to retrieve records
948    /// * Returns error if record ID parsing fails
949    /// * Returns error if record deletion fails
950    pub async fn clear_database(&self) -> Result<()> {
951        debug!("Clearing all records from the database");
952        // Get the total count of records in the database
953        let number_of_records = self.get_number_of_records().await?;
954
955        // Check if there are any records to delete
956        if number_of_records == 0 {
957            warn!("No records found in the database. Nothing to clear");
958            return Ok(());
959        }
960
961        // Retrieve all records that need to be deleted
962        // The number_of_records value is used as limit to fetch all records at once
963        let records = self.get_records(1, number_of_records).await.map_err(|e| {
964            error!("Failed to retrieve records for clearing database: {}", e);
965            anyhow::anyhow!(e)
966        })?;
967
968        // Iterate through each record and delete it individually
969        for record in records {
970            // Extract the record ID from the record data
971            if let Some(id) = record.get("recordId").and_then(|id| id.as_str()) {
972                // The record ID is usually marked as a string even though it's a u64,
973                // so we need to parse it to the correct type
974                if let Ok(id) = id.parse::<u64>() {
975                    debug!("Deleting record ID: {}", id);
976                    // Attempt to delete the record and handle any errors
977                    if let Err(e) = self.delete_record(id).await {
978                        error!("Failed to delete record ID {}: {}", id, e);
979                        return Err(anyhow::anyhow!(e));
980                    }
981                } else {
982                    // Handle case where ID exists but cannot be parsed as u64
983                    error!("Failed to parse record ID {} as u64", id);
984                    return Err(anyhow::anyhow!("Failed to parse record ID as u64"));
985                }
986            } else {
987                // Handle case where record doesn't contain an ID field
988                error!("Record ID not found in record: {:?}", record);
989                return Err(anyhow::anyhow!(
990                    "Record ID not found in record: {:?}",
991                    record
992                ));
993            }
994        }
995
996        info!("All records cleared from the database");
997        Ok(())
998    }
999    /// Returns the names of fields in the given record excluding the ones starting with 'g_' (global fields)
1000    ///
1001    /// # Arguments
1002    /// * `record` - An example record with 'fieldData' element containing field names as keys.
1003    ///
1004    /// # Returns
1005    /// An array of field names.
1006    pub fn get_row_names_by_example(record: &Value) -> Vec<String> {
1007        let mut fields = Vec::new();
1008        if let Some(field_data) = record.get("fieldData").and_then(|fd| fd.as_object()) {
1009            for field in field_data.keys() {
1010                if !field.starts_with("g_") {
1011                    fields.push(field.clone());
1012                }
1013            }
1014        }
1015        info!("Extracted row names: {:?}", fields);
1016        fields
1017    }
1018
1019    /// Gets the field names for the first record in the database.
1020    ///
1021    /// This function retrieves a single record from the database and extracts
1022    /// field names from it. If no records exist, an empty vector is returned.
1023    ///
1024    /// # Returns
1025    /// * `Result<Vec<String>>` - A vector of field names on success, or an error
1026    pub async fn get_row_names(&self) -> Result<Vec<String>> {
1027        debug!("Attempting to fetch field names for the first record");
1028
1029        // Fetch just the first record to use as a template
1030        let records = self.get_records(1, 1).await?;
1031
1032        if let Some(first_record) = records.first() {
1033            info!("Successfully fetched field names for the first record");
1034            // Extract field names from the first record using the helper method
1035            return Ok(Self::get_row_names_by_example(first_record));
1036        }
1037
1038        // Handle the case where no records exist in the database
1039        warn!("No records found while fetching field names");
1040        Ok(vec![])
1041    }
1042
1043    /// Searches the database for records matching the specified query.
1044    ///
1045    /// # Arguments
1046    /// * `fields` - The query fields.
1047    /// * `sort` - The sort order.
1048    /// * `ascending` - Whether to sort in ascending order.
1049    ///
1050    /// # Returns
1051    /// A vector of matching records.
1052    pub async fn advanced_search(
1053        &self,
1054        fields: HashMap<String, Value>,
1055        sort: Vec<String>,
1056        ascending: bool,
1057    ) -> Result<Vec<Value>> {
1058        let url = format!(
1059            "{}/databases/{}/layouts/{}/_find",
1060            Self::get_fm_url()?,
1061            self.database,
1062            self.table
1063        );
1064
1065        debug!(
1066            "Preparing advanced search with fields: {:?}, sort: {:?}, ascending: {}",
1067            fields, sort, ascending
1068        );
1069
1070        let mut content = serde_json::Map::new();
1071        content.insert(
1072            "query".to_string(),
1073            Value::Array(fields.into_iter().map(|(k, v)| json!({ k: v })).collect()),
1074        );
1075
1076        if !sort.is_empty() {
1077            let sort_array: Vec<Value> = sort
1078                .into_iter()
1079                .map(|s| {
1080                    json!({
1081                        "fieldName": s,
1082                        "sortOrder": if ascending { "ascend" } else { "descend" }
1083                    })
1084                })
1085                .collect();
1086            content.insert("sort".to_string(), Value::Array(sort_array));
1087        }
1088
1089        debug!(
1090            "Sending authenticated request to URL: {} with content: {:?}",
1091            url, content
1092        );
1093
1094        let response = self
1095            .authenticated_request(&url, Method::POST, Some(Value::Object(content)))
1096            .await?;
1097
1098        if let Some(data) = response
1099            .get("response")
1100            .and_then(|r| r.get("data"))
1101            .and_then(|d| d.as_array())
1102        {
1103            info!(
1104                "Advanced search completed successfully, retrieved {} records",
1105                data.len()
1106            );
1107            Ok(data.clone())
1108        } else {
1109            error!("Failed to retrieve advanced search results: {:?}", response);
1110            Err(anyhow::anyhow!(
1111                "Failed to retrieve advanced search results"
1112            ))
1113        }
1114    }
1115}