baserow_rs/
lib.rs

1//! A Rust client for the Baserow API
2//!
3//! This crate provides a strongly-typed client for interacting with Baserow's REST API.
4//! It supports authentication, table operations, file uploads, and more.
5//!
6//! # Example
7//! ```no_run
8//! use baserow_rs::{ConfigBuilder, Baserow, BaserowTableOperations, api::client::BaserowClient};
9//! use std::collections::HashMap;
10//! use serde_json::Value;
11//!
12//! #[tokio::main]
13//! async fn main() {
14//!     // Create a configuration
15//!     let config = ConfigBuilder::new()
16//!         .base_url("https://api.baserow.io")
17//!         .api_key("your-api-key")
18//!         .build();
19//!
20//!     // Initialize the client
21//!     let baserow = Baserow::with_configuration(config);
22//!
23//!     // Get a table reference
24//!     let table = baserow.table_by_id(1234);
25//!
26//!     // Create a record
27//!     let mut data = HashMap::new();
28//!     data.insert("Name".to_string(), Value::String("Test".to_string()));
29//!
30//!     let result = table.create_one(data, None).await.unwrap();
31//!     println!("Created record: {:?}", result);
32//! }
33//! ```
34
35use std::{error::Error, fs::File};
36
37use tracing::{debug, error, info, instrument, span, Level};
38
39use api::{
40    authentication::{LoginRequest, TokenResponse, User},
41    client::{BaserowClient, RequestTracing},
42};
43use error::{FileUploadError, TokenAuthError};
44use mapper::TableMapper;
45use reqwest::{
46    header::AUTHORIZATION,
47    multipart::{self, Form},
48    Body, Client, StatusCode,
49};
50use serde::{Deserialize, Serialize};
51use tokio_util::codec::{BytesCodec, FramedRead};
52
53pub mod api;
54
55#[macro_use]
56extern crate async_trait;
57
58pub mod error;
59pub mod filter;
60pub mod mapper;
61
62/// Configuration for the Baserow client
63///
64/// This struct holds all the configuration options needed to connect to a Baserow instance,
65/// including authentication credentials and API endpoints.
66#[derive(Clone, Debug)]
67pub struct Configuration {
68    base_url: String,
69
70    email: Option<String>,
71    password: Option<String>,
72    jwt: Option<String>,
73
74    database_token: Option<String>,
75    access_token: Option<String>,
76    refresh_token: Option<String>,
77
78    user: Option<User>,
79}
80
81/// Builder for creating Configuration instances
82///
83/// Provides a fluent interface for constructing Configuration objects with the required parameters.
84///
85/// # Example
86/// ```
87/// use baserow_rs::ConfigBuilder;
88///
89/// let config = ConfigBuilder::new()
90///     .base_url("https://api.baserow.io")
91///     .api_key("your-api-key")
92///     .build();
93/// ```
94#[derive(Default)]
95pub struct ConfigBuilder {
96    base_url: Option<String>,
97    api_key: Option<String>,
98    email: Option<String>,
99    password: Option<String>,
100}
101
102impl ConfigBuilder {
103    pub fn new() -> Self {
104        Self {
105            base_url: None,
106            api_key: None,
107            email: None,
108            password: None,
109        }
110    }
111
112    pub fn base_url(mut self, base_url: &str) -> Self {
113        self.base_url = Some(base_url.to_string());
114        self
115    }
116
117    pub fn api_key(mut self, api_key: &str) -> Self {
118        self.api_key = Some(api_key.to_string());
119        self
120    }
121
122    pub fn email(mut self, email: &str) -> Self {
123        self.email = Some(email.to_string());
124        self
125    }
126
127    pub fn password(mut self, password: &str) -> Self {
128        self.password = Some(password.to_string());
129        self
130    }
131
132    pub fn build(self) -> Configuration {
133        Configuration {
134            base_url: self.base_url.unwrap(),
135
136            email: self.email,
137            password: self.password,
138            jwt: None,
139
140            database_token: self.api_key,
141            access_token: None,
142            refresh_token: None,
143
144            user: None,
145        }
146    }
147}
148
149/// Main client for interacting with the Baserow API
150///
151/// This struct implements the BaserowClient trait and provides methods for all API operations.
152/// It handles authentication, request signing, and maintains the client state.
153#[derive(Clone, Debug)]
154pub struct Baserow {
155    configuration: Configuration,
156    client: Client,
157}
158
159impl Baserow {
160    pub fn with_configuration(configuration: Configuration) -> Self {
161        let span = span!(Level::INFO, "baserow_init");
162        let _enter = span.enter();
163
164        info!("Initializing Baserow client with configuration");
165        debug!(?configuration, "Configuration details");
166
167        Self {
168            configuration,
169            client: Client::new(),
170        }
171    }
172
173    pub fn with_database_token(self, token: String) -> Self {
174        let mut configuration = self.configuration.clone();
175        configuration.database_token = Some(token);
176
177        Self {
178            configuration,
179            client: self.client,
180        }
181    }
182
183    fn with_access_token(&self, access_token: String) -> Self {
184        let mut configuration = self.configuration.clone();
185        configuration.access_token = Some(access_token);
186
187        Self {
188            configuration,
189            client: self.client.clone(),
190        }
191    }
192
193    fn with_refresh_token(&self, refresh_token: String) -> Self {
194        let mut configuration = self.configuration.clone();
195        configuration.refresh_token = Some(refresh_token);
196
197        Self {
198            configuration,
199            client: self.client.clone(),
200        }
201    }
202
203    fn with_user(&self, user: User) -> Self {
204        let mut configuration = self.configuration.clone();
205        configuration.user = Some(user);
206
207        Self {
208            configuration,
209            client: self.client.clone(),
210        }
211    }
212}
213
214#[async_trait]
215impl BaserowClient for Baserow {
216    fn get_configuration(&self) -> Configuration {
217        self.configuration.clone()
218    }
219
220    fn get_client(&self) -> Client {
221        self.client.clone()
222    }
223
224    #[instrument(skip(self), err)]
225    async fn token_auth(&self) -> Result<Box<dyn BaserowClient>, TokenAuthError> {
226        let url = format!("{}/api/user/token-auth/", &self.configuration.base_url);
227
228        let email = self
229            .configuration
230            .email
231            .as_ref()
232            .ok_or(TokenAuthError::MissingCredentials("email"))?;
233
234        let password = self
235            .configuration
236            .password
237            .as_ref()
238            .ok_or(TokenAuthError::MissingCredentials("password"))?;
239
240        let auth_request = LoginRequest {
241            email: email.clone(),
242            password: password.clone(),
243        };
244
245        let req = Client::new().post(url).json(&auth_request);
246
247        debug!("Sending token authentication request");
248        let resp = self.trace_request(&self.client, req.build()?).await?;
249
250        match resp.status() {
251            StatusCode::OK => {
252                info!("Token authentication successful");
253                let token_response: TokenResponse = resp.json().await?;
254                let client = self
255                    .clone()
256                    .with_database_token(token_response.token)
257                    .with_access_token(token_response.access_token)
258                    .with_refresh_token(token_response.refresh_token)
259                    .with_user(token_response.user);
260                Ok(Box::new(client) as Box<dyn BaserowClient>)
261            }
262            _status => {
263                let error_text = resp.text().await?;
264                let error = TokenAuthError::AuthenticationFailed(error_text);
265                error.log();
266                Err(error)
267            }
268        }
269    }
270
271    #[instrument(skip(self), err)]
272    async fn table_fields(&self, table_id: u64) -> Result<Vec<TableField>, Box<dyn Error>> {
273        let url = format!(
274            "{}/api/database/fields/table/{}/",
275            &self.configuration.base_url, table_id
276        );
277
278        let mut req = self.client.get(url);
279
280        if let Some(token) = &self.configuration.jwt {
281            req = req.header(AUTHORIZATION, format!("JWT {}", token));
282        } else if let Some(token) = &self.configuration.database_token {
283            req = req.header(AUTHORIZATION, format!("Token {}", token));
284        } else {
285            return Err("No authentication token provided".into());
286        }
287
288        debug!("Sending request to fetch table fields");
289        let resp = self.trace_request(&self.client, req.build()?).await?;
290        match resp.status() {
291            StatusCode::OK => {
292                let fields: Vec<TableField> = resp.json().await?;
293                info!(
294                    field_count = fields.len(),
295                    "Successfully retrieved table fields"
296                );
297                debug!(?fields, "Retrieved field details");
298                Ok(fields)
299            }
300            status => {
301                let error_text = resp.text().await?;
302                error!(%status, error = %error_text, "Failed to retrieve table fields");
303                Err(format!(
304                    "Failed to retrieve table fields (status: {}): {}",
305                    status, error_text
306                )
307                .into())
308            }
309        }
310    }
311
312    fn table_by_id(&self, id: u64) -> BaserowTable {
313        BaserowTable::default()
314            .with_id(id)
315            .with_baserow(self.clone())
316    }
317
318    #[instrument(skip(self, file), fields(filename = %filename), err)]
319    async fn upload_file(
320        &self,
321        file: File,
322        filename: String,
323    ) -> Result<api::file::File, FileUploadError> {
324        let url = format!(
325            "{}/api/user-files/upload-file/",
326            &self.configuration.base_url
327        );
328
329        let file = tokio::fs::File::from_std(file);
330        let stream = FramedRead::new(file, BytesCodec::new());
331        let file_body = Body::wrap_stream(stream);
332
333        let mime_type = mime_guess::from_path(&filename).first_or_octet_stream();
334
335        let file_part = multipart::Part::stream(file_body)
336            .file_name(filename)
337            .mime_str(mime_type.as_ref())?;
338
339        let form = Form::new().part("file", file_part);
340
341        let mut req = self.client.post(url);
342
343        if let Some(token) = &self.configuration.jwt {
344            req = req.header(AUTHORIZATION, format!("JWT {}", token));
345        } else if let Some(api_key) = &self.configuration.database_token {
346            req = req.header(AUTHORIZATION, format!("Token {}", api_key));
347        }
348
349        let resp = self
350            .trace_request(&self.client, req.multipart(form).build()?)
351            .await;
352
353        match resp {
354            Ok(resp) => match resp.status() {
355                StatusCode::OK => {
356                    let json: api::file::File = resp.json().await?;
357                    info!("File upload successful");
358                    debug!(?json, "Upload response details");
359                    Ok(json)
360                }
361                status => {
362                    let error = FileUploadError::UnexpectedStatusCode(status);
363                    error.log();
364                    Err(error)
365                }
366            },
367            Err(e) => {
368                let error = FileUploadError::UploadError(e);
369                error.log();
370                Err(error)
371            }
372        }
373    }
374
375    #[instrument(skip(self), err)]
376    async fn upload_file_via_url(&self, url: &str) -> Result<api::file::File, FileUploadError> {
377        // Validate URL format and scheme
378        let file_url = url
379            .parse::<reqwest::Url>()
380            .map_err(|_| FileUploadError::InvalidURL(url.to_string()))?
381            .to_string();
382
383        let upload_request = api::file::UploadFileViaUrlRequest {
384            url: file_url.clone(),
385        };
386
387        let url = format!(
388            "{}/api/user-files/upload-via-url/",
389            &self.configuration.base_url
390        );
391
392        let mut req = self.client.post(url).json(&upload_request);
393
394        if let Some(token) = &self.configuration.jwt {
395            req = req.header(AUTHORIZATION, format!("JWT {}", token));
396        } else if let Some(api_key) = &self.configuration.database_token {
397            req = req.header(AUTHORIZATION, format!("Token {}", api_key));
398        }
399
400        let resp = self.trace_request(&self.client, req.build()?).await;
401
402        match resp {
403            Ok(resp) => match resp.status() {
404                StatusCode::OK => {
405                    let json: api::file::File = resp.json().await?;
406                    info!("File upload via URL successful");
407                    debug!(?json, "Upload response details");
408                    Ok(json)
409                }
410                status => {
411                    let error = FileUploadError::UnexpectedStatusCode(status);
412                    error.log();
413                    Err(error)
414                }
415            },
416            Err(e) => {
417                let error = FileUploadError::UploadError(e);
418                error.log();
419                Err(error)
420            }
421        }
422    }
423}
424
425/// Represents a table in Baserow
426///
427/// This struct provides methods for interacting with a specific table, including
428/// creating, reading, updating and deleting records.
429#[derive(Deserialize, Serialize, Default, Clone)]
430pub struct BaserowTable {
431    #[serde(skip)]
432    baserow: Option<Baserow>,
433
434    #[serde(skip)]
435    mapper: Option<TableMapper>,
436
437    id: Option<u64>,
438    pub name: Option<String>,
439    order: Option<i64>,
440    database_id: Option<i64>,
441}
442
443impl BaserowTable {
444    fn with_baserow(mut self, baserow: Baserow) -> BaserowTable {
445        self.baserow = Some(baserow);
446        self
447    }
448
449    fn with_id(mut self, id: u64) -> BaserowTable {
450        self.id = Some(id);
451        self
452    }
453}
454
455pub use api::table_operations::BaserowTableOperations;
456
457/// Represents a field in a Baserow table
458///
459/// Contains metadata about a table column including its type, name, and other attributes.
460#[derive(Clone, Deserialize, Serialize, Debug)]
461pub struct TableField {
462    pub id: u64,
463    pub table_id: u64,
464    pub name: String,
465    pub order: u32,
466    pub r#type: String,
467    pub primary: bool,
468    pub read_only: bool,
469    pub description: Option<String>,
470}
471
472/// Specifies the sort direction for table queries
473///
474/// Used when ordering table results to determine ascending or descending order.
475#[derive(Clone, Debug)]
476pub enum OrderDirection {
477    Asc,
478    Desc,
479}
480
481#[cfg(test)]
482mod tests {
483    use super::*;
484    use serde_json::Value;
485    use std::collections::HashMap;
486
487    #[test]
488    fn test() {
489        let configuration = Configuration {
490            base_url: "https://baserow.io".to_string(),
491            database_token: Some("123".to_string()),
492            email: None,
493            password: None,
494            jwt: None,
495            access_token: None,
496            refresh_token: None,
497            user: None,
498        };
499        let baserow = Baserow::with_configuration(configuration);
500        let _table = baserow.table_by_id(1234);
501    }
502
503    #[tokio::test]
504    async fn test_create_record() {
505        let mut server = mockito::Server::new_async().await;
506        let mock_url = server.url();
507
508        let mock = server
509            .mock("POST", "/api/database/rows/table/1234/")
510            .with_status(200)
511            .with_header("Content-Type", "application/json")
512            .with_header(AUTHORIZATION, format!("Token {}", "123").as_str())
513            .with_body(r#"{"id": 1234, "field_1": "test"}"#)
514            .create();
515
516        let configuration = Configuration {
517            base_url: mock_url,
518            database_token: Some("123".to_string()),
519            email: None,
520            password: None,
521            jwt: None,
522            access_token: None,
523            refresh_token: None,
524            user: None,
525        };
526        let baserow = Baserow::with_configuration(configuration);
527        let table = baserow.table_by_id(1234);
528
529        let mut record = HashMap::new();
530        record.insert("field_1".to_string(), Value::String("test".to_string()));
531
532        let result = table.create_one(record, None).await;
533        assert!(result.is_ok());
534
535        let created_record = result.unwrap();
536        assert_eq!(created_record["field_1"], Value::String("test".to_string()));
537
538        mock.assert();
539    }
540
541    #[tokio::test]
542    async fn test_get_record() {
543        let mut server = mockito::Server::new_async().await;
544        let mock_url = server.url();
545
546        let mock = server
547            .mock("GET", "/api/database/rows/table/1234/5678/")
548            .with_status(200)
549            .with_header("Content-Type", "application/json")
550            .with_header(AUTHORIZATION, format!("Token {}", "123").as_str())
551            .with_body(r#"{"id": 5678, "field_1": "test"}"#)
552            .create();
553
554        let configuration = Configuration {
555            base_url: mock_url,
556            database_token: Some("123".to_string()),
557            email: None,
558            password: None,
559            jwt: None,
560            access_token: None,
561            refresh_token: None,
562            user: None,
563        };
564        let baserow = Baserow::with_configuration(configuration);
565        let table = baserow.table_by_id(1234);
566
567        let result: Result<HashMap<String, Value>, Box<dyn Error>> =
568            table.get_one(5678, None).await;
569        assert!(result.is_ok());
570
571        let record = result.unwrap();
572        assert_eq!(record["id"], Value::Number(5678.into()));
573        assert_eq!(record["field_1"], Value::String("test".to_string()));
574
575        mock.assert();
576    }
577
578    #[tokio::test]
579    async fn test_update_record() {
580        let mut server = mockito::Server::new_async().await;
581        let mock_url = server.url();
582
583        let mock = server
584            .mock("PATCH", "/api/database/rows/table/1234/5678/")
585            .with_status(200)
586            .with_header("Content-Type", "application/json")
587            .with_header(AUTHORIZATION, format!("Token {}", "123").as_str())
588            .with_body(r#"{"id": 5678, "field_1": "updated"}"#)
589            .create();
590
591        let configuration = Configuration {
592            base_url: mock_url,
593            database_token: Some("123".to_string()),
594            email: None,
595            password: None,
596            jwt: None,
597            access_token: None,
598            refresh_token: None,
599            user: None,
600        };
601        let baserow = Baserow::with_configuration(configuration);
602        let table = baserow.table_by_id(1234);
603
604        let mut record = HashMap::new();
605        record.insert("field_1".to_string(), Value::String("updated".to_string()));
606
607        let result = table.update(5678, record, None).await;
608        assert!(result.is_ok());
609
610        let updated_record = result.unwrap();
611        assert_eq!(
612            updated_record["field_1"],
613            Value::String("updated".to_string())
614        );
615
616        mock.assert();
617    }
618
619    #[tokio::test]
620    async fn test_delete_record() {
621        let mut server = mockito::Server::new_async().await;
622        let mock_url = server.url();
623
624        let mock = server
625            .mock("DELETE", "/api/database/rows/table/1234/5678/")
626            .with_status(200)
627            .with_header("Content-Type", "application/json")
628            .with_header(AUTHORIZATION, format!("Token {}", "123").as_str())
629            .create();
630
631        let configuration = Configuration {
632            base_url: mock_url,
633            database_token: Some("123".to_string()),
634            email: None,
635            password: None,
636            jwt: None,
637            access_token: None,
638            refresh_token: None,
639            user: None,
640        };
641        let baserow = Baserow::with_configuration(configuration);
642        let table = baserow.table_by_id(1234);
643
644        let result = table.delete(5678).await;
645        assert!(result.is_ok());
646
647        mock.assert();
648    }
649
650    #[tokio::test]
651    async fn test_upload_file_via_url() {
652        let mut server = mockito::Server::new_async().await;
653        let mock_url = server.url();
654
655        let mock = server
656            .mock("POST", "/api/user-files/upload-via-url/")
657            .with_status(200)
658            .with_header("Content-Type", "application/json")
659            .with_header(AUTHORIZATION, format!("Token {}", "123").as_str())
660            .with_body(r#"{
661    "url": "https://files.baserow.io/user_files/VXotniBOVm8tbstZkKsMKbj2Qg7KmPvn_39d354a76abe56baaf569ad87d0333f58ee4bf3eed368e3b9dc736fd18b09dfd.png",
662    "thumbnails": {
663        "tiny": {
664            "url": "https://files.baserow.io/media/thumbnails/tiny/VXotniBOVm8tbstZkKsMKbj2Qg7KmPvn_39d354a76abe56baaf569ad87d0333f58ee4bf3eed368e3b9dc736fd18b09dfd.png",
665            "width": 21,
666            "height": 21
667        },
668        "small": {
669            "url": "https://files.baserow.io/media/thumbnails/small/VXotniBOVm8tbstZkKsMKbj2Qg7KmPvn_39d354a76abe56baaf569ad87d0333f58ee4bf3eed368e3b9dc736fd18b09dfd.png",
670            "width": 48,
671            "height": 48
672        }
673    },
674    "name": "VXotniBOVm8tbstZkKsMKbj2Qg7KmPvn_39d354a76abe56baaf569ad87d0333f58ee4bf3eed368e3b9dc736fd18b09dfd.png",
675    "size": 229940,
676    "mime_type": "image/png",
677    "is_image": true,
678    "image_width": 1280,
679    "image_height": 585,
680    "uploaded_at": "2020-11-17T12:16:10.035234+00:00"
681}"#)
682            .create();
683
684        let configuration = Configuration {
685            base_url: mock_url,
686            database_token: Some("123".to_string()),
687            email: None,
688            password: None,
689            jwt: None,
690            access_token: None,
691            refresh_token: None,
692            user: None,
693        };
694        let baserow = Baserow::with_configuration(configuration);
695
696        let result = baserow
697            .upload_file_via_url("https://example.com/test.txt")
698            .await;
699        assert!(result.is_ok());
700
701        let uploaded_file = result.unwrap();
702        assert_eq!(uploaded_file.name, "VXotniBOVm8tbstZkKsMKbj2Qg7KmPvn_39d354a76abe56baaf569ad87d0333f58ee4bf3eed368e3b9dc736fd18b09dfd.png".to_string());
703
704        mock.assert();
705    }
706
707    #[tokio::test]
708    async fn test_token_auth() {
709        let mut server = mockito::Server::new_async().await;
710        let mock_url = server.url();
711
712        let mock = server
713            .mock("POST", "/api/user/token-auth/")
714            .with_status(200)
715            .with_header("Content-Type", "application/json")
716            .with_body(
717                r#"{
718  "user": {
719    "first_name": "string",
720    "username": "user@example.com",
721    "language": "string"
722  },
723  "token": "string",
724  "access_token": "string",
725  "refresh_token": "string"
726}"#,
727            )
728            .create();
729
730        let configuration = ConfigBuilder::new()
731            .base_url(&mock_url)
732            .email("test@example.com")
733            .password("password")
734            .build();
735        let baserow = Baserow::with_configuration(configuration);
736
737        let result = baserow.token_auth().await;
738        assert!(result.is_ok());
739
740        let logged_in_baserow = result.unwrap();
741        assert_eq!(
742            logged_in_baserow
743                .get_configuration()
744                .database_token
745                .unwrap(),
746            "string"
747        );
748
749        mock.assert();
750    }
751
752    #[tokio::test]
753    async fn test_upload_file() {
754        let mut server = mockito::Server::new_async().await;
755        let mock_url = server.url();
756
757        let mock = server
758            .mock("POST", "/api/user-files/upload-file/")
759            .with_status(200)
760            .with_header("Content-Type", "application/json")
761            .with_header(AUTHORIZATION, format!("Token {}", "123").as_str())
762            .with_body(r#"{
763    "url": "https://files.baserow.io/user_files/VXotniBOVm8tbstZkKsMKbj2Qg7KmPvn_39d354a76abe56baaf569ad87d0333f58ee4bf3eed368e3b9dc736fd18b09dfd.png",
764    "thumbnails": {
765        "tiny": {
766            "url": "https://files.baserow.io/media/thumbnails/tiny/VXotniBOVm8tbstZkKsMKbj2Qg7KmPvn_39d354a76abe56baaf569ad87d0333f58ee4bf3eed368e3b9dc736fd18b09dfd.png",
767            "width": 21,
768            "height": 21
769        },
770        "small": {
771            "url": "https://files.baserow.io/media/thumbnails/small/VXotniBOVm8tbstZkKsMKbj2Qg7KmPvn_39d354a76abe56baaf569ad87d0333f58ee4bf3eed368e3b9dc736fd18b09dfd.png",
772            "width": 48,
773            "height": 48
774        }
775    },
776    "name": "VXotniBOVm8tbstZkKsMKbj2Qg7KmPvn_39d354a76abe56baaf569ad87d0333f58ee4bf3eed368e3b9dc736fd18b09dfd.png",
777    "size": 229940,
778    "mime_type": "image/png",
779    "is_image": true,
780    "image_width": 1280,
781    "image_height": 585,
782    "uploaded_at": "2020-11-17T12:16:10.035234+00:00"
783}"#)
784            .create();
785
786        let configuration = Configuration {
787            base_url: mock_url,
788            database_token: Some("123".to_string()),
789            email: None,
790            password: None,
791            jwt: None,
792            access_token: None,
793            refresh_token: None,
794            user: None,
795        };
796        let baserow = Baserow::with_configuration(configuration);
797
798        let file = File::open(".gitignore").unwrap();
799        let result = baserow.upload_file(file, "image.png".to_string()).await;
800        assert!(result.is_ok());
801
802        let uploaded_file = result.unwrap();
803        assert_eq!(uploaded_file.name, "VXotniBOVm8tbstZkKsMKbj2Qg7KmPvn_39d354a76abe56baaf569ad87d0333f58ee4bf3eed368e3b9dc736fd18b09dfd.png".to_string());
804
805        mock.assert();
806    }
807
808    #[tokio::test]
809    async fn test_view_query() {
810        let mut server = mockito::Server::new_async().await;
811        let mock_url = server.url();
812
813        let mock = server
814            .mock("GET", "/api/database/rows/table/1234/")
815            .match_query(mockito::Matcher::UrlEncoded("view_id".into(), "5678".into()))
816            .with_status(200)
817            .with_header("Content-Type", "application/json")
818            .with_header(AUTHORIZATION, format!("Token {}", "123").as_str())
819            .with_body(r#"{"count": 1, "next": null, "previous": null, "results": [{"id": 1, "field_1": "test"}]}"#)
820            .create();
821
822        let configuration = Configuration {
823            base_url: mock_url,
824            database_token: Some("123".to_string()),
825            email: None,
826            password: None,
827            jwt: None,
828            access_token: None,
829            refresh_token: None,
830            user: None,
831        };
832        let baserow = Baserow::with_configuration(configuration);
833        let table = baserow.table_by_id(1234);
834
835        let result = table
836            .query()
837            .view(5678)
838            .get::<HashMap<String, Value>>()
839            .await;
840
841        assert!(result.is_ok());
842        let response = result.unwrap();
843        assert_eq!(response.count, Some(1));
844        assert_eq!(
845            response.results[0]["field_1"],
846            Value::String("test".to_string())
847        );
848
849        mock.assert();
850    }
851
852    #[tokio::test]
853    async fn test_view_query_with_filters() {
854        let mut server = mockito::Server::new_async().await;
855        let mock_url = server.url();
856
857        let mock = server
858            .mock("GET", "/api/database/rows/table/1234/")
859            .match_query(mockito::Matcher::AllOf(vec![
860                mockito::Matcher::UrlEncoded("view_id".into(), "5678".into()),
861                mockito::Matcher::UrlEncoded("filter__field_1__equal".into(), "test".into()),
862                mockito::Matcher::UrlEncoded("order_by".into(), "field_1".into()),
863            ]))
864            .with_status(200)
865            .with_header("Content-Type", "application/json")
866            .with_header(AUTHORIZATION, format!("Token {}", "123").as_str())
867            .with_body(r#"{"count": 1, "next": null, "previous": null, "results": [{"id": 1, "field_1": "test"}]}"#)
868            .create();
869
870        let configuration = Configuration {
871            base_url: mock_url,
872            database_token: Some("123".to_string()),
873            email: None,
874            password: None,
875            jwt: None,
876            access_token: None,
877            refresh_token: None,
878            user: None,
879        };
880        let baserow = Baserow::with_configuration(configuration);
881        let table = baserow.table_by_id(1234);
882
883        let result = table
884            .query()
885            .view(5678)
886            .filter_by("field_1", filter::Filter::Equal, "test")
887            .order_by("field_1", OrderDirection::Asc)
888            .get::<HashMap<String, Value>>()
889            .await;
890
891        assert!(result.is_ok());
892        let response = result.unwrap();
893        assert_eq!(response.count, Some(1));
894        assert_eq!(
895            response.results[0]["field_1"],
896            Value::String("test".to_string())
897        );
898
899        mock.assert();
900    }
901
902    #[tokio::test]
903    async fn test_view_query_with_pagination() {
904        let mut server = mockito::Server::new_async().await;
905        let mock_url = server.url();
906
907        let mock = server
908            .mock("GET", "/api/database/rows/table/1234/")
909            .match_query(mockito::Matcher::AllOf(vec![
910                mockito::Matcher::UrlEncoded("view_id".into(), "5678".into()),
911                mockito::Matcher::UrlEncoded("size".into(), "2".into()),
912                mockito::Matcher::UrlEncoded("page".into(), "1".into()),
913            ]))
914            .with_status(200)
915            .with_header("Content-Type", "application/json")
916            .with_header(AUTHORIZATION, format!("Token {}", "123").as_str())
917            .with_body(r#"{"count": 3, "next": "http://example.com/next", "previous": "http://example.com/prev", "results": [{"id": 2, "field_1": "test2"}, {"id": 3, "field_1": "test3"}]}"#)
918            .create();
919
920        let configuration = Configuration {
921            base_url: mock_url,
922            database_token: Some("123".to_string()),
923            email: None,
924            password: None,
925            jwt: None,
926            access_token: None,
927            refresh_token: None,
928            user: None,
929        };
930        let baserow = Baserow::with_configuration(configuration);
931        let table = baserow.table_by_id(1234);
932
933        let result = table
934            .query()
935            .view(5678)
936            .size(2)
937            .page(1)
938            .get::<HashMap<String, Value>>()
939            .await;
940
941        assert!(result.is_ok());
942        let response = result.unwrap();
943        assert_eq!(response.count, Some(3));
944        assert_eq!(response.next, Some("http://example.com/next".to_string()));
945        assert_eq!(
946            response.previous,
947            Some("http://example.com/prev".to_string())
948        );
949        assert_eq!(response.results.len(), 2);
950        assert_eq!(
951            response.results[0]["field_1"],
952            Value::String("test2".to_string())
953        );
954        assert_eq!(
955            response.results[1]["field_1"],
956            Value::String("test3".to_string())
957        );
958
959        mock.assert();
960    }
961
962    #[tokio::test]
963    async fn test_query_with_user_field_names() {
964        let mut server = mockito::Server::new_async().await;
965        let mock_url = server.url();
966
967        let mock = server
968            .mock("GET", "/api/database/rows/table/1234/")
969            .match_query(mockito::Matcher::UrlEncoded(
970                "user_field_names".into(),
971                "true".into(),
972            ))
973            .with_status(200)
974            .with_header("Content-Type", "application/json")
975            .with_header(AUTHORIZATION, format!("Token {}", "123").as_str())
976            .with_body(r#"{"count": 1, "next": null, "previous": null, "results": [{"User Name": "test"}]}"#)
977            .create();
978
979        let configuration = Configuration {
980            base_url: mock_url,
981            database_token: Some("123".to_string()),
982            email: None,
983            password: None,
984            jwt: None,
985            access_token: None,
986            refresh_token: None,
987            user: None,
988        };
989        let baserow = Baserow::with_configuration(configuration);
990        let table = baserow.table_by_id(1234);
991
992        let result = table
993            .query()
994            .user_field_names(true)
995            .get::<HashMap<String, Value>>()
996            .await;
997
998        assert!(result.is_ok());
999        let response = result.unwrap();
1000        assert_eq!(response.count, Some(1));
1001        assert_eq!(
1002            response.results[0]["User Name"],
1003            Value::String("test".to_string())
1004        );
1005
1006        mock.assert();
1007    }
1008
1009    #[tokio::test]
1010    async fn test_view_query_invalid_view() {
1011        let mut server = mockito::Server::new_async().await;
1012        let mock_url = server.url();
1013
1014        let mock = server
1015            .mock("GET", "/api/database/rows/table/1234/")
1016            .match_query(mockito::Matcher::UrlEncoded(
1017                "view_id".into(),
1018                "9999".into(),
1019            ))
1020            .with_status(404)
1021            .with_header("Content-Type", "application/json")
1022            .with_header(AUTHORIZATION, format!("Token {}", "123").as_str())
1023            .with_body(r#"{"error": "View does not exist."}"#)
1024            .create();
1025
1026        let configuration = Configuration {
1027            base_url: mock_url,
1028            database_token: Some("123".to_string()),
1029            email: None,
1030            password: None,
1031            jwt: None,
1032            access_token: None,
1033            refresh_token: None,
1034            user: None,
1035        };
1036        let baserow = Baserow::with_configuration(configuration);
1037        let table = baserow.table_by_id(1234);
1038
1039        let result = table
1040            .query()
1041            .view(9999)
1042            .get::<HashMap<String, Value>>()
1043            .await;
1044
1045        assert!(result.is_err());
1046        mock.assert();
1047    }
1048
1049    #[tokio::test]
1050    async fn test_table_fields() {
1051        let mut server = mockito::Server::new_async().await;
1052        let mock_url = server.url();
1053
1054        let mock = server
1055            .mock("GET", "/api/database/fields/table/1234/")
1056            .with_status(200)
1057            .with_header("Content-Type", "application/json")
1058            .with_header(AUTHORIZATION, format!("Token {}", "123").as_str())
1059            .with_body(
1060                r#"[
1061    {
1062        "id": 1529,
1063        "table_id": 1234,
1064        "name": "Name",
1065        "order": 0,
1066        "type": "text",
1067        "primary": true,
1068        "read_only": false,
1069        "description": "A sample description"
1070    },
1071    {
1072        "id": 6499,
1073        "table_id": 1234,
1074        "name": "Field 2",
1075        "order": 1,
1076        "type": "last_modified",
1077        "primary": false,
1078        "read_only": true,
1079        "description": "A sample description"
1080    },
1081    {
1082        "id": 6500,
1083        "table_id": 1234,
1084        "name": "Datei",
1085        "order": 2,
1086        "type": "file",
1087        "primary": false,
1088        "read_only": false,
1089        "description": "A sample description"
1090    }
1091]"#,
1092            )
1093            .create();
1094
1095        let configuration = Configuration {
1096            base_url: mock_url,
1097            database_token: Some("123".to_string()),
1098            email: None,
1099            password: None,
1100            jwt: None,
1101            access_token: None,
1102            refresh_token: None,
1103            user: None,
1104        };
1105        let baserow = Baserow::with_configuration(configuration);
1106
1107        let result = baserow.table_fields(1234).await;
1108
1109        print!("result: {:#?}", result);
1110
1111        assert!(result.is_ok());
1112
1113        let fields = result.unwrap();
1114        assert_eq!(fields.len(), 3);
1115        assert_eq!(fields[0].id, 1529);
1116        assert_eq!(fields[0].table_id, 1234);
1117        assert_eq!(fields[0].name, "Name");
1118        assert_eq!(fields[1].id, 6499);
1119        assert_eq!(fields[1].table_id, 1234);
1120        assert_eq!(fields[1].name, "Field 2");
1121
1122        mock.assert();
1123    }
1124}