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