baserow_rs/api/
table_operations.rs

1use crate::{
2    api::client::{BaserowClient, RequestTracing},
3    filter::{Filter, FilterTriple},
4    mapper::{FieldMapper, TableMapper},
5    Baserow, BaserowTable, OrderDirection,
6};
7use async_trait::async_trait;
8use reqwest::{header::AUTHORIZATION, Client, StatusCode};
9use serde::{de::DeserializeOwned, Deserialize, Serialize};
10use serde_json::Value;
11use std::{collections::HashMap, error::Error, vec};
12use tracing::{debug, info, instrument};
13
14/// Response structure for table row queries
15///
16/// Contains the query results along with pagination information.
17#[derive(Deserialize, Serialize, Debug)]
18pub struct RowsResponse {
19    /// Total count of records that match the query criteria, not just the current page
20    pub count: Option<i32>,
21    /// URL for the next page of results, if available
22    pub next: Option<String>,
23    /// URL for the previous page of results, if available
24    pub previous: Option<String>,
25    /// The actual rows returned by the query
26    pub results: Vec<HashMap<String, Value>>,
27}
28
29/// Response structure for typed table row queries
30///
31/// Contains the query results along with pagination information, where results
32/// are deserialized into the specified type.
33#[derive(Deserialize, Serialize, Debug)]
34pub struct TypedRowsResponse<T> {
35    /// Total count of records that match the query criteria, not just the current page
36    pub count: Option<i32>,
37    /// URL for the next page of results, if available
38    pub next: Option<String>,
39    /// URL for the previous page of results, if available
40    pub previous: Option<String>,
41    /// The actual rows returned by the query, deserialized into type T
42    pub results: Vec<T>,
43}
44
45/// Represents a query request for table rows
46///
47/// This struct encapsulates all the parameters that can be used to query rows
48/// from a Baserow table, including filtering, sorting, and pagination options.
49#[derive(Clone, Debug)]
50pub struct RowRequest {
51    /// Optional view ID to query rows from a specific view
52    pub view_id: Option<i32>,
53    /// Optional sorting criteria
54    pub order: Option<HashMap<String, OrderDirection>>,
55    /// Optional filter conditions
56    pub filter: Option<Vec<FilterTriple>>,
57    /// Optional page size for pagination
58    pub page_size: Option<i32>,
59    /// Optional page number for pagination
60    pub page: Option<i32>,
61    /// Optional flag to use user-friendly field names in the response
62    pub user_field_names: Option<bool>,
63}
64
65impl Default for RowRequest {
66    fn default() -> Self {
67        Self {
68            view_id: None,
69            order: None,
70            filter: None,
71            page_size: Some(100),
72            page: Some(1),
73            user_field_names: None,
74        }
75    }
76}
77
78/// Builder for constructing table row queries
79///
80/// Provides a fluent interface for building queries with filtering, sorting,
81/// and other options.
82#[derive(Default)]
83pub struct RowRequestBuilder {
84    baserow: Option<Baserow>,
85    table: Option<BaserowTable>,
86    request: RowRequest,
87}
88
89impl RowRequestBuilder {
90    pub fn new() -> Self {
91        Self {
92            baserow: None,
93            table: None,
94            request: RowRequest::default(),
95        }
96    }
97
98    /// Set the view ID to query rows from a specific view
99    pub fn view(mut self, id: i32) -> Self {
100        self.request.view_id = Some(id);
101        self
102    }
103
104    /// Set the number of rows to return per page
105    ///
106    /// # Arguments
107    /// * `size` - The number of rows per page (must be positive)
108    pub fn size(mut self, size: i32) -> Self {
109        self.request.page_size = Some(size);
110        self
111    }
112
113    /// Set the page number for pagination
114    ///
115    /// # Arguments
116    /// * `page` - The page number (must be positive)
117    pub fn page(mut self, page: i32) -> Self {
118        self.request.page = Some(page);
119        self
120    }
121
122    /// Set whether to use user-friendly field names in the response
123    ///
124    /// Note: This option is mutually exclusive with auto_map(). If auto_map() has been called,
125    /// this setting will be ignored as field name mapping is handled by the TableMapper.
126    pub fn user_field_names(mut self, enabled: bool) -> Self {
127        // Only set user_field_names if we don't have a mapper
128        if self.table.as_ref().map_or(true, |t| t.mapper.is_none()) {
129            self.request.user_field_names = Some(enabled);
130        }
131        self
132    }
133
134    pub fn with_table(mut self, table: BaserowTable) -> Self {
135        self.table = Some(table);
136        self
137    }
138
139    pub fn with_baserow(mut self, baserow: Baserow) -> Self {
140        self.baserow = Some(baserow);
141        self
142    }
143
144    /// Add sorting criteria to the query
145    pub fn order_by(mut self, field: &str, direction: OrderDirection) -> Self {
146        match self.request.order {
147            Some(mut order) => {
148                order.insert(String::from(field), direction);
149                self.request.order = Some(order);
150            }
151            None => {
152                let mut order = HashMap::new();
153                order.insert(String::from(field), direction);
154                self.request.order = Some(order);
155            }
156        }
157        self
158    }
159
160    /// Add a filter condition to the query
161    pub fn filter_by(mut self, field: &str, filter_op: Filter, value: &str) -> Self {
162        match self.request.filter {
163            Some(mut filter) => {
164                filter.push(FilterTriple {
165                    field: String::from(field),
166                    filter: filter_op,
167                    value: String::from(value),
168                });
169                self.request.filter = Some(filter);
170            }
171            None => {
172                let mut filter: Vec<FilterTriple> = vec![];
173                filter.push(FilterTriple {
174                    field: String::from(field),
175                    filter: filter_op,
176                    value: String::from(value),
177                });
178                self.request.filter = Some(filter);
179            }
180        }
181        self
182    }
183
184    /// Execute the query and return typed results
185    pub async fn get<T>(self) -> Result<TypedRowsResponse<T>, Box<dyn Error>>
186    where
187        T: DeserializeOwned + 'static,
188    {
189        let table = self.table.expect("Table instance is missing");
190        let baserow = self.baserow.expect("Baserow instance is missing");
191        table.get(baserow, self.request).await
192    }
193}
194
195/// Trait defining the public operations available on a Baserow table
196///
197/// This trait provides the core CRUD operations for working with Baserow tables.
198/// All operations are async and return Results to handle potential errors.
199#[async_trait]
200pub trait BaserowTableOperations {
201    /// Automatically maps the table fields to their corresponding types
202    ///
203    /// This method fetches the table schema and sets up field mappings for type conversion.
204    /// Call this before performing operations if you need type-safe field access.
205    async fn auto_map(self) -> Result<BaserowTable, Box<dyn Error>>;
206
207    /// Creates a new query builder for constructing complex table queries
208    ///
209    /// This is the preferred method for building queries with filters, sorting,
210    /// and pagination options. The builder provides a fluent interface for
211    /// constructing queries.
212    fn query(self) -> RowRequestBuilder;
213
214    /// Execute a row request and return typed results
215    ///
216    /// # Type Parameters
217    /// * `T` - The type to deserialize the results into
218    ///
219    /// # Arguments
220    /// * `request` - The query parameters encapsulated in a RowRequest
221    ///
222    /// # Returns
223    /// A TypedRowsResponse containing the query results and pagination information
224    async fn get<T>(
225        &self,
226        baserow: Baserow,
227        request: RowRequest,
228    ) -> Result<TypedRowsResponse<T>, Box<dyn Error>>
229    where
230        T: DeserializeOwned + 'static;
231
232    /// Creates a single record in the table
233    ///
234    /// # Arguments
235    /// * `data` - A map of field names to values representing the record to create
236    /// * `user_field_names` - Whether to use user-friendly field names in the response
237    ///
238    /// # Returns
239    /// The created record including any auto-generated fields (like ID)
240    async fn create_one(
241        self,
242        data: HashMap<String, Value>,
243        user_field_names: Option<bool>,
244    ) -> Result<HashMap<String, Value>, Box<dyn Error>>;
245
246    /// Retrieves a single record from the table by ID
247    ///
248    /// # Type Parameters
249    /// * `T` - The type to deserialize into
250    ///
251    /// # Arguments
252    /// * `id` - The unique identifier of the record to retrieve
253    /// * `user_field_names` - Whether to use user-friendly field names in the response
254    ///
255    /// # Returns
256    /// The requested record if found
257    async fn get_one<T>(self, id: u64, user_field_names: Option<bool>) -> Result<T, Box<dyn Error>>
258    where
259        T: DeserializeOwned + 'static;
260
261    /// Updates a single record in the table
262    ///
263    /// # Arguments
264    /// * `id` - The unique identifier of the record to update
265    /// * `data` - A map of field names to new values
266    /// * `user_field_names` - Whether to use user-friendly field names in the response
267    ///
268    /// # Returns
269    /// The updated record
270    async fn update(
271        self,
272        id: u64,
273        data: HashMap<String, Value>,
274        user_field_names: Option<bool>,
275    ) -> Result<HashMap<String, Value>, Box<dyn Error>>;
276
277    /// Deletes a single record from the table
278    ///
279    /// # Arguments
280    /// * `id` - The unique identifier of the record to delete
281    async fn delete(self, id: u64) -> Result<(), Box<dyn Error>>;
282}
283
284#[async_trait]
285impl BaserowTableOperations for BaserowTable {
286    #[instrument(skip(self), fields(table_id = ?self.id), err)]
287    async fn auto_map(mut self) -> Result<BaserowTable, Box<dyn Error>> {
288        let id = self.id.ok_or("Table ID is missing")?;
289
290        let baserow = self.baserow.clone().ok_or("Baserow instance is missing")?;
291        debug!("Fetching table fields for mapping");
292        let fields = baserow.table_fields(id).await?;
293        info!(
294            field_count = fields.len(),
295            "Successfully mapped table fields"
296        );
297
298        let mut mapper = TableMapper::new();
299        mapper.map_fields(fields.clone());
300        self.mapper = Some(mapper);
301
302        Ok(self)
303    }
304
305    fn query(self) -> RowRequestBuilder {
306        RowRequestBuilder::new()
307            .with_baserow(self.baserow.clone().unwrap())
308            .with_table(self.clone())
309    }
310
311    #[instrument(skip(self, baserow), fields(table_id = ?self.id), err)]
312    async fn get<T>(
313        &self,
314        baserow: Baserow,
315        request: RowRequest,
316    ) -> Result<TypedRowsResponse<T>, Box<dyn Error>>
317    where
318        T: DeserializeOwned + 'static,
319    {
320        // Validate pagination parameters
321        if let Some(size) = request.page_size {
322            if size <= 0 {
323                return Err("Page size must be a positive integer".into());
324            }
325        }
326        if let Some(page) = request.page {
327            if page <= 0 {
328                return Err("Page number must be a positive integer".into());
329            }
330        }
331
332        let url = format!(
333            "{}/api/database/rows/table/{}/",
334            &baserow.configuration.base_url,
335            self.id.unwrap()
336        );
337
338        let mut req = Client::new().get(url);
339
340        if let Some(view_id) = request.view_id {
341            req = req.query(&[("view_id", view_id.to_string())]);
342        }
343
344        if baserow.configuration.jwt.is_some() {
345            req = req.header(
346                AUTHORIZATION,
347                format!(
348                    "JWT {}",
349                    &baserow.configuration.database_token.as_ref().unwrap()
350                ),
351            );
352        } else if baserow.configuration.database_token.is_some() {
353            req = req.header(
354                AUTHORIZATION,
355                format!(
356                    "Token {}",
357                    &baserow.configuration.database_token.as_ref().unwrap()
358                ),
359            );
360        }
361
362        if let Some(order) = request.order {
363            let mut order_str = String::new();
364            for (field, direction) in order {
365                // Map field name to ID if auto_map is enabled
366                let field_key = if let Some(mapper) = &self.mapper {
367                    if let Some(field_id) = mapper.get_field_id(&field) {
368                        format!("field_{}", field_id)
369                    } else {
370                        field
371                    }
372                } else {
373                    field
374                };
375
376                order_str.push_str(&format!(
377                    "{}{}",
378                    match direction {
379                        OrderDirection::Asc => "",
380                        OrderDirection::Desc => "-",
381                    },
382                    field_key
383                ));
384            }
385
386            req = req.query(&[("order_by", order_str)]);
387        }
388
389        if let Some(filter) = request.filter {
390            for triple in filter {
391                // Map field name to ID if auto_map is enabled
392                let field_key = if let Some(mapper) = &self.mapper {
393                    if let Some(field_id) = mapper.get_field_id(&triple.field) {
394                        format!("field_{}", field_id)
395                    } else {
396                        triple.field
397                    }
398                } else {
399                    triple.field
400                };
401
402                req = req.query(&[(
403                    &format!("filter__{}__{}", field_key, triple.filter.as_str()),
404                    triple.value,
405                )]);
406            }
407        }
408
409        if let Some(size) = request.page_size {
410            req = req.query(&[("size", size.to_string())]);
411        }
412
413        if let Some(page) = request.page {
414            req = req.query(&[("page", page.to_string())]);
415        }
416
417        if let Some(user_field_names) = request.user_field_names {
418            req = req.query(&[("user_field_names", user_field_names.to_string())]);
419        }
420
421        debug!("Executing table query");
422        let resp = baserow.trace_request(&baserow.client, req.build()?).await?;
423
424        match resp.status() {
425            StatusCode::OK => {
426                let response: RowsResponse = resp.json().await?;
427
428                let typed_results = if let Some(mapper) = &self.mapper {
429                    // When using auto_map, convert field IDs to names first
430                    response
431                        .results
432                        .into_iter()
433                        .map(|row| mapper.deserialize_row(row))
434                        .collect::<Result<Vec<T>, _>>()?
435                } else {
436                    // When not using auto_map, try direct deserialization
437                    serde_json::from_value::<Vec<T>>(Value::Array(
438                        response
439                            .results
440                            .into_iter()
441                            .map(|m| Value::Object(serde_json::Map::from_iter(m.into_iter())))
442                            .collect(),
443                    ))?
444                };
445
446                Ok(TypedRowsResponse {
447                    count: response.count,
448                    next: response.next,
449                    previous: response.previous,
450                    results: typed_results,
451                })
452            }
453            _ => Err(Box::new(resp.error_for_status().unwrap_err())),
454        }
455    }
456
457    #[instrument(skip(self, data), fields(table_id = ?self.id, field_count = data.len()), err)]
458    async fn create_one(
459        self,
460        data: HashMap<String, Value>,
461        user_field_names: Option<bool>,
462    ) -> Result<HashMap<String, Value>, Box<dyn Error>> {
463        let baserow = self.baserow.expect("Baserow instance is missing");
464
465        let url = format!(
466            "{}/api/database/rows/table/{}/",
467            &baserow.configuration.base_url,
468            self.id.unwrap()
469        );
470
471        let mut req = baserow.client.post(url);
472
473        // Convert field names to IDs if auto_map is enabled
474        let request_data = if self.mapper.is_some() {
475            self.mapper.as_ref().unwrap().convert_to_field_ids(data)
476        } else {
477            data
478        };
479
480        // Use user_field_names parameter for response format
481        if let Some(use_names) = user_field_names {
482            req = req.query(&[("user_field_names", use_names.to_string())]);
483        }
484
485        if baserow.configuration.jwt.is_some() {
486            req = req.header(
487                AUTHORIZATION,
488                format!("JWT {}", &baserow.configuration.jwt.as_ref().unwrap()),
489            );
490        } else if baserow.configuration.database_token.is_some() {
491            req = req.header(
492                AUTHORIZATION,
493                format!(
494                    "Token {}",
495                    &baserow.configuration.database_token.as_ref().unwrap()
496                ),
497            );
498        }
499
500        debug!("Creating new record");
501        let resp = baserow
502            .trace_request(&baserow.client, req.json(&request_data).build()?)
503            .await?;
504
505        match resp.status() {
506            StatusCode::OK => {
507                let response_data = resp.json::<HashMap<String, Value>>().await?;
508
509                // Convert response field IDs to names if auto_map is enabled
510                if self.mapper.is_some() && user_field_names != Some(true) {
511                    Ok(self
512                        .mapper
513                        .as_ref()
514                        .unwrap()
515                        .convert_to_field_names(response_data))
516                } else {
517                    Ok(response_data)
518                }
519            }
520            _ => Err(Box::new(resp.error_for_status().unwrap_err())),
521        }
522    }
523
524    #[instrument(skip(self), fields(table_id = ?self.id, record_id = %id), err)]
525    async fn get_one<T>(
526        mut self,
527        id: u64,
528        user_field_names: Option<bool>,
529    ) -> Result<T, Box<dyn Error>>
530    where
531        T: DeserializeOwned + 'static,
532    {
533        let baserow = self.baserow.expect("Baserow instance is missing");
534
535        let url = format!(
536            "{}/api/database/rows/table/{}/{}/",
537            &baserow.configuration.base_url,
538            self.id.unwrap(),
539            id
540        );
541
542        let mut req = baserow.client.get(url);
543
544        if let Some(use_names) = user_field_names {
545            req = req.query(&[("user_field_names", use_names.to_string())]);
546        }
547
548        if baserow.configuration.jwt.is_some() {
549            req = req.header(
550                AUTHORIZATION,
551                format!("JWT {}", &baserow.configuration.jwt.as_ref().unwrap()),
552            );
553        } else if baserow.configuration.database_token.is_some() {
554            req = req.header(
555                AUTHORIZATION,
556                format!(
557                    "Token {}",
558                    &baserow.configuration.database_token.as_ref().unwrap()
559                ),
560            );
561        }
562
563        debug!("Fetching single record");
564        let resp = baserow.trace_request(&baserow.client, req.build()?).await?;
565
566        match resp.status() {
567            StatusCode::OK => {
568                let row: HashMap<String, Value> = resp.json().await?;
569
570                // For HashMap<String, Value>, use serde to convert
571                if std::any::TypeId::of::<T>() == std::any::TypeId::of::<HashMap<String, Value>>() {
572                    Ok(serde_json::from_value(serde_json::to_value(row)?)?)
573                } else {
574                    // For other types, use the mapper if available
575                    let mapper = self.mapper.clone().ok_or("Table mapper is missing. Call auto_map() first when using typed responses.")?;
576                    Ok(mapper.deserialize_row(row)?)
577                }
578            }
579            _ => Err(Box::new(resp.error_for_status().unwrap_err())),
580        }
581    }
582
583    #[instrument(skip(self, data), fields(table_id = ?self.id, record_id = %id, field_count = data.len()), err)]
584    async fn update(
585        self,
586        id: u64,
587        data: HashMap<String, Value>,
588        user_field_names: Option<bool>,
589    ) -> Result<HashMap<String, Value>, Box<dyn Error>> {
590        let baserow = self.baserow.expect("Baserow instance is missing");
591
592        let url = format!(
593            "{}/api/database/rows/table/{}/{}/",
594            &baserow.configuration.base_url,
595            self.id.unwrap(),
596            id
597        );
598
599        let mut req = baserow.client.patch(url);
600
601        // Convert field names to IDs if auto_map is enabled
602        let request_data = if self.mapper.is_some() {
603            self.mapper.as_ref().unwrap().convert_to_field_ids(data)
604        } else {
605            data
606        };
607
608        // Use user_field_names parameter for response format
609        if let Some(use_names) = user_field_names {
610            req = req.query(&[("user_field_names", use_names.to_string())]);
611        }
612
613        if baserow.configuration.jwt.is_some() {
614            req = req.header(
615                AUTHORIZATION,
616                format!("JWT {}", &baserow.configuration.jwt.as_ref().unwrap()),
617            );
618        } else if baserow.configuration.database_token.is_some() {
619            req = req.header(
620                AUTHORIZATION,
621                format!(
622                    "Token {}",
623                    &baserow.configuration.database_token.as_ref().unwrap()
624                ),
625            );
626        }
627
628        debug!("Updating record");
629        let resp = baserow
630            .trace_request(&baserow.client, req.json(&request_data).build()?)
631            .await?;
632
633        match resp.status() {
634            StatusCode::OK => {
635                let response_data = resp.json::<HashMap<String, Value>>().await?;
636
637                // Convert response field IDs to names if auto_map is enabled
638                if self.mapper.is_some() && user_field_names != Some(true) {
639                    Ok(self
640                        .mapper
641                        .as_ref()
642                        .unwrap()
643                        .convert_to_field_names(response_data))
644                } else {
645                    Ok(response_data)
646                }
647            }
648            _ => Err(Box::new(resp.error_for_status().unwrap_err())),
649        }
650    }
651
652    #[instrument(skip(self), fields(table_id = ?self.id, record_id = %id), err)]
653    async fn delete(self, id: u64) -> Result<(), Box<dyn Error>> {
654        let baserow = self.baserow.expect("Baserow instance is missing");
655
656        let url = format!(
657            "{}/api/database/rows/table/{}/{}/",
658            &baserow.configuration.base_url,
659            self.id.unwrap(),
660            id
661        );
662
663        let mut req = baserow.client.delete(url);
664
665        if baserow.configuration.jwt.is_some() {
666            req = req.header(
667                AUTHORIZATION,
668                format!("JWT {}", &baserow.configuration.jwt.as_ref().unwrap()),
669            );
670        } else if baserow.configuration.database_token.is_some() {
671            req = req.header(
672                AUTHORIZATION,
673                format!(
674                    "Token {}",
675                    &baserow.configuration.database_token.as_ref().unwrap()
676                ),
677            );
678        }
679
680        debug!("Deleting record");
681        let resp = baserow.trace_request(&baserow.client, req.build()?).await?;
682
683        match resp.status() {
684            StatusCode::OK => Ok(()),
685            _ => Err(Box::new(resp.error_for_status().unwrap_err())),
686        }
687    }
688}
689
690#[cfg(test)]
691mod tests {
692    use super::*;
693    use crate::{
694        api::client::BaserowClient, filter::Filter, Baserow, BaserowTableOperations, ConfigBuilder,
695        OrderDirection,
696    };
697    use serde::Deserialize;
698    use serde_json::Value;
699    use std::collections::HashMap;
700
701    #[derive(Debug, Deserialize, PartialEq)]
702    struct TestUser {
703        name: String,
704    }
705
706    #[derive(Debug, Deserialize, PartialEq)]
707    struct ComplexRecord {
708        id: u64,
709        name: String,
710        email: String,
711        age: Option<i32>,
712        is_active: bool,
713        created_at: String,
714    }
715
716    #[tokio::test]
717    async fn test_collection_deserialization() {
718        let mut server = mockito::Server::new_async().await;
719        let mock_url = server.url();
720
721        // Mock the fields endpoint for auto_map
722        let fields_mock = server
723            .mock("GET", "/api/database/fields/table/1234/")
724            .with_status(200)
725            .with_header("Content-Type", "application/json")
726            .with_body(r#"[
727                {"id": 1, "table_id": 1234, "name": "id", "order": 0, "type": "number", "primary": true, "read_only": false},
728                {"id": 2, "table_id": 1234, "name": "name", "order": 1, "type": "text", "primary": false, "read_only": false},
729                {"id": 3, "table_id": 1234, "name": "email", "order": 2, "type": "text", "primary": false, "read_only": false},
730                {"id": 4, "table_id": 1234, "name": "age", "order": 3, "type": "number", "primary": false, "read_only": false},
731                {"id": 5, "table_id": 1234, "name": "is_active", "order": 4, "type": "boolean", "primary": false, "read_only": false},
732                {"id": 6, "table_id": 1234, "name": "created_at", "order": 5, "type": "text", "primary": false, "read_only": false}
733            ]"#)
734            .create();
735
736        // Mock the rows endpoint with multiple records
737        let rows_mock = server
738            .mock("GET", "/api/database/rows/table/1234/")
739            .match_query(mockito::Matcher::Any)
740            .with_status(200)
741            .with_header("Content-Type", "application/json")
742            .with_body(
743                r#"{
744                "count": 2,
745                "next": null,
746                "previous": null,
747                "results": [
748                    {
749                        "field_1": 101,
750                        "field_2": "John Doe",
751                        "field_3": "john@example.com",
752                        "field_4": 30,
753                        "field_5": true,
754                        "field_6": "2023-01-01T00:00:00Z"
755                    },
756                    {
757                        "field_1": 102,
758                        "field_2": "Jane Smith",
759                        "field_3": "jane@example.com",
760                        "field_4": null,
761                        "field_5": false,
762                        "field_6": "2023-01-02T00:00:00Z"
763                    }
764                ]
765            }"#,
766            )
767            .create();
768
769        let configuration = ConfigBuilder::new()
770            .base_url(&mock_url)
771            .api_key("test-token")
772            .build();
773        let baserow = Baserow::with_configuration(configuration);
774        let table = baserow.table_by_id(1234);
775
776        // Test deserialization of multiple records with complex types
777        let mapped_table = table.auto_map().await.unwrap();
778        let response = mapped_table.query().get::<ComplexRecord>().await.unwrap();
779
780        assert_eq!(response.count, Some(2));
781        assert_eq!(response.results.len(), 2);
782
783        // Verify first record
784        let record1 = &response.results[0];
785        assert_eq!(record1.id, 101);
786        assert_eq!(record1.name, "John Doe");
787        assert_eq!(record1.email, "john@example.com");
788        assert_eq!(record1.age, Some(30));
789        assert_eq!(record1.is_active, true);
790        assert_eq!(record1.created_at, "2023-01-01T00:00:00Z");
791
792        // Verify second record with null field
793        let record2 = &response.results[1];
794        assert_eq!(record2.id, 102);
795        assert_eq!(record2.name, "Jane Smith");
796        assert_eq!(record2.email, "jane@example.com");
797        assert_eq!(record2.age, None);
798        assert_eq!(record2.is_active, false);
799        assert_eq!(record2.created_at, "2023-01-02T00:00:00Z");
800
801        fields_mock.assert();
802        rows_mock.assert();
803    }
804
805    #[tokio::test]
806    async fn test_auto_map_and_user_field_names_exclusivity() {
807        let mut server = mockito::Server::new_async().await;
808        let mock_url = server.url();
809
810        // Mock the fields endpoint
811        let fields_mock = server
812            .mock("GET", "/api/database/fields/table/1234/")
813            .with_status(200)
814            .with_header("Content-Type", "application/json")
815            .with_body(r#"[{"id": 1, "table_id": 1234, "name": "Name", "order": 0, "type": "text", "primary": true, "read_only": false}]"#)
816            .create();
817
818        // Mock the rows endpoint
819        let rows_mock = server
820            .mock("GET", "/api/database/rows/table/1234/")
821            .match_query(mockito::Matcher::Any)
822            .expect_at_least(1)
823            .with_status(200)
824            .with_header("Content-Type", "application/json")
825            .with_body(
826                r#"{"count": 1, "next": null, "previous": null, "results": [{"field_1": "test"}]}"#,
827            )
828            .create();
829
830        let configuration = ConfigBuilder::new()
831            .base_url(&mock_url)
832            .api_key("test-token")
833            .build();
834        let baserow = Baserow::with_configuration(configuration);
835        let table = baserow.table_by_id(1234);
836
837        // First test: auto_map should take precedence over user_field_names
838        let mapped_table = table.clone().auto_map().await.unwrap();
839        let _query = mapped_table
840            .query()
841            .user_field_names(true) // This should be ignored since we have auto_map
842            .get::<HashMap<String, Value>>()
843            .await
844            .unwrap();
845
846        // Verify that user_field_names parameter was not included in the request
847        rows_mock.assert();
848        fields_mock.assert();
849    }
850
851    #[tokio::test]
852    async fn test_field_mapping_in_query_params() {
853        let mut server = mockito::Server::new_async().await;
854        let mock_url = server.url();
855
856        // Mock the fields endpoint
857        let fields_mock = server
858            .mock("GET", "/api/database/fields/table/1234/")
859            .with_status(200)
860            .with_header("Content-Type", "application/json")
861            .with_body(r#"[
862                {"id": 1, "table_id": 1234, "name": "name", "order": 0, "type": "text", "primary": true, "read_only": false},
863                {"id": 2, "table_id": 1234, "name": "age", "order": 1, "type": "number", "primary": false, "read_only": false}
864            ]"#)
865            .create();
866
867        // Mock the rows endpoint with field ID mapping
868        let rows_mock = server
869            .mock("GET", "/api/database/rows/table/1234/")
870            .match_query(mockito::Matcher::AllOf(vec![
871                mockito::Matcher::UrlEncoded(
872                    "order_by".into(),
873                    "field_1".into(), // Should use field_1 instead of "name"
874                ),
875                mockito::Matcher::UrlEncoded(
876                    "filter__field_2__equal".into(), // Should use field_2 instead of "age"
877                    "25".into(),
878                ),
879            ]))
880            .with_status(200)
881            .with_header("Content-Type", "application/json")
882            .with_body(r#"{"count": 1, "next": null, "previous": null, "results": [{"field_1": "John", "field_2": 25}]}"#)
883            .create();
884
885        let configuration = ConfigBuilder::new()
886            .base_url(&mock_url)
887            .api_key("test-token")
888            .build();
889        let baserow = Baserow::with_configuration(configuration);
890        let table = baserow.table_by_id(1234);
891
892        // Test that field names are properly mapped to IDs in query parameters
893        let mapped_table = table.auto_map().await.unwrap();
894        let _result = mapped_table
895            .query()
896            .order_by("name", OrderDirection::Asc)
897            .filter_by("age", Filter::Equal, "25")
898            .get::<HashMap<String, Value>>()
899            .await
900            .unwrap();
901
902        // Verify that the request was made with mapped field IDs
903        fields_mock.assert();
904        rows_mock.assert();
905    }
906
907    #[tokio::test]
908    async fn test_struct_deserialization_with_both_options() {
909        let mut server = mockito::Server::new_async().await;
910        let mock_url = server.url();
911
912        // Mock the fields endpoint for auto_map
913        let fields_mock = server
914            .mock("GET", "/api/database/fields/table/1234/")
915            .with_status(200)
916            .with_header("Content-Type", "application/json")
917            .with_body(r#"[{"id": 1, "table_id": 1234, "name": "name", "order": 0, "type": "text", "primary": true, "read_only": false}]"#)
918            .create();
919
920        // Mock the rows endpoint for auto_map test
921        let rows_mock_auto_map = server
922            .mock("GET", "/api/database/rows/table/1234/")
923            .match_query(mockito::Matcher::Any)
924            .with_status(200)
925            .with_header("Content-Type", "application/json")
926            .with_body(
927                r#"{"count": 1, "next": null, "previous": null, "results": [{"field_1": "John"}]}"#,
928            )
929            .create();
930
931        // Mock the rows endpoint for user_field_names test
932        let rows_mock_user_names = server
933            .mock("GET", "/api/database/rows/table/1234/")
934            .match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
935                "user_field_names".into(),
936                "true".into(),
937            )]))
938            .with_status(200)
939            .with_header("Content-Type", "application/json")
940            .with_body(
941                r#"{"count": 1, "next": null, "previous": null, "results": [{"name": "John"}]}"#,
942            )
943            .create();
944
945        let configuration = ConfigBuilder::new()
946            .base_url(&mock_url)
947            .api_key("test-token")
948            .build();
949        let baserow = Baserow::with_configuration(configuration);
950        let table = baserow.table_by_id(1234);
951
952        // Test auto_map deserialization
953        let mapped_table = table.clone().auto_map().await.unwrap();
954        let auto_map_result = mapped_table.query().get::<TestUser>().await.unwrap();
955
956        assert_eq!(
957            auto_map_result.results[0],
958            TestUser {
959                name: "John".to_string()
960            }
961        );
962
963        // Test user_field_names deserialization
964        let user_names_result = table
965            .query()
966            .user_field_names(true)
967            .get::<TestUser>()
968            .await
969            .unwrap();
970
971        assert_eq!(
972            user_names_result.results[0],
973            TestUser {
974                name: "John".to_string()
975            }
976        );
977
978        // Verify the mocks were called the expected number of times
979        fields_mock.assert();
980        rows_mock_auto_map.assert();
981        rows_mock_user_names.assert();
982    }
983}