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