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