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