Skip to main content

larkrs_client/bitable/
table.rs

1#![allow(dead_code)]
2
3use crate::LarkApiResponse;
4use crate::auth::FeishuTokenManager;
5use crate::bitable::{FieldsListResponse, SearchRecordsResponse};
6use anyhow::{Result, anyhow};
7use reqwest::Client;
8use serde_json::Value;
9use thiserror::Error;
10
11use super::BatchCreateRecordsRequest;
12
13#[derive(Error, Debug)]
14pub enum BitableApiError {
15    #[error("Network error: {0}")]
16    NetworkError(#[from] reqwest::Error),
17
18    #[error("JSON serialization error: {0}")]
19    SerdeError(#[from] serde_json::Error),
20
21    #[error("API error: {message} (code: {code})")]
22    ApiError { code: i32, message: String },
23}
24
25pub struct BitableTableClient {
26    token_manager: FeishuTokenManager,
27}
28
29impl BitableTableClient {
30    pub fn new() -> Self {
31        Self {
32            token_manager: FeishuTokenManager::new(),
33        }
34    }
35
36    /// Search records in a Bitable table
37    ///
38    /// See: https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-record/search
39    pub async fn get_records_list(
40        &self,
41        app_token: &str,
42        table_id: &str,
43        request: super::SearchRecordsCond,
44    ) -> Result<SearchRecordsResponse> {
45        let url = format!(
46            "https://open.feishu.cn/open-apis/bitable/v1/apps/{}/tables/{}/records/search",
47            app_token, table_id
48        );
49
50        let token = self.token_manager.get_token().await?;
51        let response = Client::new()
52            .post(&url)
53            .header("Authorization", format!("Bearer {}", token))
54            .header("Content-Type", "application/json; charset=utf-8")
55            .json(&request)
56            .send()
57            .await
58            .map_err(|e| anyhow!(e).context("Failed to send request for searching records"))?
59            .json::<LarkApiResponse<SearchRecordsResponse>>()
60            .await
61            .map_err(|e| anyhow!(e).context("Failed to parse search records response"))?;
62
63        if !response.is_success() {
64            let err = BitableApiError::ApiError {
65                code: response.code,
66                message: response.msg.clone(),
67            };
68            return Err(anyhow!(err).context(format!("API returned error code: {}", response.code)));
69        }
70
71        Ok(response.data)
72    }
73
74    /// Batch create multiple records in a Bitable table
75    ///
76    /// * [Feishu Bitable Batch Create API](https://open.feishu.cn/document/server-docs/docs/bitable-v1/app-table-record/batch_create)
77    pub async fn batch_create_records(
78        &self,
79        app_token: &str,
80        table_id: &str,
81        request: BatchCreateRecordsRequest,
82    ) -> Result<()> {
83        if app_token.is_empty() || table_id.is_empty() {
84            return Err(anyhow!("app_token and table_id cannot be empty"));
85        }
86        if request.records.is_empty() {
87            return Err(anyhow!("No records provided for batch creation"));
88        }
89
90        let url = format!(
91            "https://open.feishu.cn/open-apis/bitable/v1/apps/{}/tables/{}/records/batch_create",
92            app_token, table_id
93        );
94        let token = self
95            .token_manager
96            .get_token()
97            .await
98            .map_err(|e| anyhow!(e).context("Failed to obtain authentication token"))?;
99
100        let resp = Client::new()
101            .post(&url)
102            .header("Authorization", format!("Bearer {}", token))
103            .header("Content-Type", "application/json; charset=utf-8")
104            .json(&request)
105            .send()
106            .await
107            .map_err(|e| anyhow!(e).context("Failed to send request for batch creating records"))?
108            .json::<LarkApiResponse<Value>>()
109            .await
110            .map_err(|e| anyhow!(e).context("Failed to parse batch create records response"))?;
111
112        match resp.is_success() {
113            true => Ok(()),
114            false => Err(anyhow!(BitableApiError::ApiError {
115                code: resp.code,
116                message: resp.msg.clone(),
117            })
118            .context(format!(
119                "API returned error code: {} - {}",
120                resp.code, resp.msg
121            ))),
122        }
123    }
124
125    pub async fn batch_create_records_json(
126        &self,
127        app_token: &str,
128        table_id: &str,
129        records_json: &str,
130    ) -> Result<()> {
131        if app_token.is_empty() || table_id.is_empty() {
132            return Err(anyhow!("app_token and table_id cannot be empty"));
133        }
134
135        // 先尝试解析JSON字符串
136        let value: Value = serde_json::from_str(records_json)
137            .map_err(|e| anyhow!("Failed to parse JSON string: {}", e))?;
138
139        // 使用From trait将Value转换为BatchCreateRecordsRequest
140        let request = BatchCreateRecordsRequest::from(value);
141
142        if request.records.is_empty() {
143            return Err(anyhow!("No valid records found in the provided JSON"));
144        }
145
146        self.batch_create_records(app_token, table_id, request)
147            .await
148    }
149
150    pub async fn get_fields_list(
151        &self,
152        app_token: &str,
153        table_id: &str,
154    ) -> Result<FieldsListResponse> {
155        if app_token.is_empty() || table_id.is_empty() {
156            return Err(anyhow!("app_token and table_id cannot be empty"));
157        }
158
159        let token = self.token_manager.get_token().await?;
160
161        let url = format!(
162            "https://open.feishu.cn/open-apis/bitable/v1/apps/{}/tables/{}/fields",
163            app_token, table_id
164        );
165
166        let response = Client::new()
167            .get(&url)
168            .header("Authorization", format!("Bearer {}", token))
169            .header("Content-Type", "application/json; charset=utf-8")
170            .send()
171            .await
172            .map_err(|e| anyhow!(e).context("Failed to send request for getting fields list"))?
173            .json::<LarkApiResponse<FieldsListResponse>>()
174            .await
175            .map_err(|e| anyhow!(e).context("Failed to parse fields list response"))?;
176
177        match response.is_success() {
178            true => Ok(response.data),
179            false => Err(anyhow!(BitableApiError::ApiError {
180                code: response.code,
181                message: response.msg.clone(),
182            })
183            .context(format!(
184                "API returned error code: {} - {}",
185                response.code, response.msg
186            ))),
187        }
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194    use crate::bitable::{
195        Filter, FilterCondition, FilterConjunction, FilterOperator, SearchRecordsCond,
196    };
197
198    #[tokio::test]
199    async fn test_get_records_list() {
200        dotenvy::dotenv().ok();
201
202        let client = BitableTableClient::new();
203
204        let app_token = "xxx";
205        let table_id = "xxx";
206        let view_id = "xxx";
207        let result = client
208            .get_records_list(
209                &app_token,
210                &table_id,
211                SearchRecordsCond {
212                    view_id: view_id.to_string(),
213                    filter: Some(Filter {
214                        conditions: vec![FilterCondition {
215                            field_name: "战法".to_string(),
216                            operator: FilterOperator::Is,
217                            value: vec!["战法A".to_string()],
218                        }],
219                        conjunction: FilterConjunction::And,
220                    }),
221                    ..Default::default()
222                },
223            )
224            .await;
225        assert!(result.is_ok());
226
227        println!("Result: {:#?}", result.unwrap());
228    }
229
230    #[tokio::test]
231    async fn test_batch_create_records() {
232        dotenvy::dotenv().ok();
233
234        let client = BitableTableClient::new();
235
236        let app_token = "xxxx";
237        let table_id = "xxxx";
238
239        let records_json = r#"[
240            {
241                "股票名称": "xxxx",
242                "题材概念": "xxxx",
243                "日期": 1743129600000,
244                "梯队": ["xxxx"]
245            }
246        ]"#;
247        let result = client
248            .batch_create_records_json(app_token, table_id, records_json)
249            .await;
250        assert!(result.is_ok());
251
252        println!("Result: {:#?}", result.unwrap());
253    }
254
255    #[tokio::test]
256    async fn test_get_fields_list() {
257        dotenvy::dotenv().ok();
258
259        let client = BitableTableClient::new();
260
261        let app_token = "xxxx";
262        let table_id = "xxxx";
263
264        let result = client.get_fields_list(app_token, table_id).await;
265        assert!(result.is_ok());
266
267        let fields: Vec<crate::bitable::FieldInfo> = result.unwrap().into();
268        println!("Simplified Field Infos: {:#?}", fields);
269    }
270}