filemaker_lib/
lib.rs

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