A wrapper around reqwest to make working with Pocketbase a breeze
at develop 17 kB view raw
1use base64::{Engine, prelude::BASE64_STANDARD_NO_PAD}; 2use reqwest::{Method, header::HeaderMap}; 3use serde::{Deserialize, Serialize, de::DeserializeOwned}; 4use serde_json::{Map, Value, json}; 5 6use crate::{ 7 Error, JsonObject, PocketBase, 8 auth::{ 9 AuthMethodList, AuthRefreshBuilder, AuthWithOtpBuilder, AuthWithPasswordBuilder, 10 ImpersonateBuilder, 11 }, 12}; 13 14const NO_PARAMS: &[&str] = &[]; 15 16pub struct CreateRequestBuilder<'a, T: Serialize> { 17 record_service: RecordService<'a>, 18 body: T, 19} 20 21pub struct DeleteRequestBuilder<'a> { 22 record_service: RecordService<'a>, 23 id: &'a str, 24} 25 26#[derive(Clone, Debug)] 27pub struct ListRequestBuilder<'a> { 28 record_service: RecordService<'a>, 29 expand_fields: Option<&'a str>, 30 filter_fields: Option<&'a str>, 31 filter_records: Option<&'a str>, 32 page: usize, 33 per_page: usize, 34 skip_total: Option<bool>, 35 sort: Option<&'a str>, 36} 37 38#[derive(Clone, Debug)] 39pub struct FullListRequestBuilder<'a> { 40 record_service: RecordService<'a>, 41 batch: Option<usize>, 42 expand_fields: Option<&'a str>, 43 filter_fields: Option<&'a str>, 44 filter_records: Option<&'a str>, 45 skip_total: Option<bool>, 46 order_by: Option<&'a str>, 47} 48 49#[derive(Clone, Debug)] 50pub struct RecordService<'a> { 51 pub(super) client: &'a PocketBase, 52 pub(super) collection: &'a str, 53} 54 55#[derive(Clone, Debug, Default, Deserialize, Eq, Serialize, PartialEq)] 56#[serde(rename_all = "camelCase")] 57pub struct ResultList<T> { 58 items: Vec<T>, 59 page: usize, 60 per_page: usize, 61 total_items: isize, 62 total_pages: isize, 63} 64 65#[derive(Clone, Debug)] 66pub struct ViewRequestBuilder<'a> { 67 record_service: RecordService<'a>, 68 expand_fields: Option<&'a str>, 69 filter_fields: Option<&'a str>, 70 id: &'a str, 71} 72 73#[derive(Clone, Debug)] 74pub struct UpdateRequestBuilder<'a, T: Serialize> { 75 record_service: RecordService<'a>, 76 body: T, 77 expand_fields: Option<&'a str>, 78 filter_fields: Option<&'a str>, 79 id: &'a str, 80} 81 82impl<'a, T: Serialize + Clone> CreateRequestBuilder<'a, T> { 83 pub fn object<X: Serialize + Clone>(self, obj: X) -> CreateRequestBuilder<'a, X> { 84 CreateRequestBuilder { 85 record_service: self.record_service, 86 body: obj, 87 } 88 } 89 90 pub async fn send<R: DeserializeOwned>(self) -> Result<R, Error> { 91 let params: &[()] = &[]; 92 93 self.record_service 94 .client 95 .send( 96 &format!( 97 "/api/collections/{}/records", 98 self.record_service.collection 99 ), 100 Method::POST, 101 HeaderMap::new(), 102 &params, 103 Some(&self.body), 104 ) 105 .await 106 } 107} 108 109impl<T: Extend<(String, Value)> + Serialize> CreateRequestBuilder<'_, T> { 110 pub fn fields<Iter: IntoIterator<Item = (String, Value)>>(mut self, patch: Iter) -> Self { 111 self.body.extend(patch); 112 self 113 } 114} 115 116impl DeleteRequestBuilder<'_> { 117 pub async fn send<T: DeserializeOwned>(self) -> Result<T, Error> { 118 let params: &[()] = &[]; 119 self.record_service 120 .client 121 .send( 122 &format!( 123 "/api/collections/{}/records/{}", 124 self.record_service.collection, self.id 125 ), 126 Method::DELETE, 127 HeaderMap::new(), 128 &params, 129 None::<&()>, 130 ) 131 .await 132 // TODO: Dart SDK also implements the following behaviour. 133 // 134 // If the current [`AuthStore.record`] matches with the deleted id, then on 135 // success the client [`AuthStore`] will be also cleared. 136 } 137} 138 139impl<'a> FullListRequestBuilder<'a> { 140 pub fn batch_size(mut self, batch: usize) -> Self { 141 self.batch = Some(batch); 142 self 143 } 144 145 pub fn expand_fields(mut self, field_expression: &'a str) -> Self { 146 self.expand_fields = Some(field_expression); 147 self 148 } 149 150 pub fn filter_fields(mut self, field_expression: &'a str) -> Self { 151 self.filter_fields = Some(field_expression); 152 self 153 } 154 155 pub fn filter_records(mut self, filter_expression: &'a str) -> Self { 156 self.filter_records = Some(filter_expression); 157 self 158 } 159 160 pub fn skip_total(mut self, skip_total: bool) -> Self { 161 self.skip_total = Some(skip_total); 162 self 163 } 164 165 pub fn order_by(mut self, sort_expression: &'a str) -> Self { 166 self.order_by = Some(sort_expression); 167 self 168 } 169 170 pub async fn send<T: DeserializeOwned>(self) -> Result<Vec<T>, Error> { 171 let mut list = ListRequestBuilder { 172 record_service: self.record_service, 173 expand_fields: self.expand_fields, 174 filter_fields: self.filter_fields, 175 filter_records: self.filter_records, 176 page: 1, 177 per_page: self.batch.unwrap_or(500), 178 skip_total: Some(true), 179 sort: self.order_by, 180 }; 181 182 let mut ret = Vec::new(); 183 loop { 184 let page = list.clone().send().await?; 185 let persist = page.items.len() == list.per_page; 186 ret.extend(page.items); 187 if !persist { 188 break; 189 } 190 list.page += 1; 191 } 192 Ok(ret) 193 } 194} 195 196impl<'a> ListRequestBuilder<'a> { 197 pub fn expand_fields(mut self, field_expression: &'a str) -> Self { 198 self.expand_fields = Some(field_expression); 199 self 200 } 201 202 pub fn filter_fields(mut self, field_expression: &'a str) -> Self { 203 self.filter_fields = Some(field_expression); 204 self 205 } 206 207 pub fn filter(mut self, filter: &'a str) -> Self { 208 self.filter_records = Some(filter); 209 self 210 } 211 212 pub fn skip_total(mut self, skip_total: bool) -> Self { 213 self.skip_total = Some(skip_total); 214 self 215 } 216 217 pub fn sort(mut self, sort: &'a str) -> Self { 218 self.sort = Some(sort); 219 self 220 } 221 222 pub async fn send<T: DeserializeOwned>(self) -> Result<ResultList<T>, Error> { 223 let mut storage = vec![self.page.to_string(), self.per_page.to_string()]; 224 if let Some(skip_total) = self.skip_total { 225 storage.push(skip_total.to_string()); 226 } 227 let mut params: Vec<(&str, &str)> = vec![("page", &storage[0]), ("perPage", &storage[1])]; 228 if let Some(filter) = self.filter_records { 229 params.push(("filter", filter)); 230 } 231 if let Some(sort) = self.sort { 232 params.push(("sort", sort)); 233 } 234 if let Some(expand) = self.expand_fields { 235 params.push(("expand", expand)); 236 } 237 if let Some(fields) = self.filter_fields { 238 params.push(("fields", fields)); 239 } 240 if self.skip_total.is_some() { 241 params.push(("skipTotal", &storage[2])); 242 } 243 244 self.record_service 245 .client 246 .send( 247 &format!( 248 "/api/collections/{}/records", 249 self.record_service.collection 250 ), 251 Method::GET, 252 HeaderMap::new(), 253 &params, 254 None::<&()>, 255 ) 256 .await 257 } 258} 259 260impl<'a> RecordService<'a> { 261 /// Refreshes the current authenticated auth record instance and returns a new 262 /// token and record data. 263 /// 264 /// On success this method automatically updates the client's AuthStore. 265 pub fn auth_refresh(&self) -> AuthRefreshBuilder<'a> { 266 AuthRefreshBuilder { 267 record_service: self.clone(), 268 expand_fields: None, 269 filter_fields: None, 270 } 271 } 272 273 /// Authenticate an auth record via OTP. 274 /// 275 /// On success this method automatically updates the client's [`AuthStore`]. 276 pub fn auth_with_otp(&'a self, otp_id: &'a str, password: &'a str) -> AuthWithOtpBuilder<'a> { 277 AuthWithOtpBuilder { 278 record_service: self.clone(), 279 otp_id, 280 password, 281 expand_fields: None, 282 filter_fields: None, 283 } 284 } 285 286 /// Authenticate an auth record by its username/email and password and returns 287 /// a new auth token and record data. 288 /// 289 /// On success this method automatically updates the client's [`AuthStore`]. 290 pub fn auth_with_password( 291 &self, 292 identity: &'a str, 293 password: &'a str, 294 ) -> AuthWithPasswordBuilder<'a> { 295 AuthWithPasswordBuilder { 296 record_service: self.clone(), 297 identity, 298 password, 299 expand_fields: None, 300 filter_fields: None, 301 } 302 } 303 304 /// Confirms auth record new email address. 305 /// 306 /// If the current [`AuthStore.record`] matches with the record from the 307 /// token, then on success the client [`AuthStore`] will be also cleared. 308 pub async fn confirm_email_change(&self, token: &str, password: &str) -> Result<(), Error> { 309 let () = self 310 .client 311 .send( 312 &format!("/api/collections/{}/confirm-email-change", self.collection), 313 Method::POST, 314 HeaderMap::new(), 315 NO_PARAMS, 316 Some(&json!({ "token": token, "password": password })), 317 ) 318 .await?; 319 320 let parts: Vec<_> = token.split(".").collect(); 321 if parts.len() != 3 { 322 return Ok(()); 323 } 324 let Ok(payload_str) = String::from_utf8(BASE64_STANDARD_NO_PAD.decode(parts[1]).unwrap()) 325 else { 326 return Ok(()); 327 }; 328 let Ok(payload) = serde_json::from_str::<JsonObject>(&payload_str) else { 329 return Ok(()); 330 }; 331 332 { 333 let inner = self.client.inner.read().unwrap(); 334 if let Some(auth) = inner.auth_store.as_ref() { 335 if auth.record.data.get("id") == payload.get("id") 336 && auth.record.data.get("collectionId") == payload.get("collectionId") 337 { 338 self.client.auth_clear(); 339 } 340 } 341 } 342 Ok(()) 343 } 344 345 /// Confirms auth record password reset request. 346 pub async fn confirm_password_reset( 347 &self, 348 token: &str, 349 password: &str, 350 password_confirm: &str, 351 ) -> Result<(), Error> { 352 self.client 353 .send( 354 &format!( 355 "/api/collections/{}/confirm-password-reset", 356 self.collection 357 ), 358 Method::POST, 359 HeaderMap::new(), 360 NO_PARAMS, 361 Some( 362 &json!({ "token": token, "password": password, "passwordConfirm": password_confirm }), 363 ), 364 ) 365 .await 366 } 367 368 /// Confirms auth record email verification request. 369 /// 370 /// On success this method automatically updates the client's [`AuthStore`]. 371 pub async fn confirm_verification(&self, token: &str) -> Result<(), Error> { 372 let () = self 373 .client 374 .send( 375 &format!("/api/collections/{}/confirm-verification", self.collection), 376 Method::POST, 377 HeaderMap::new(), 378 NO_PARAMS, 379 Some(&json!({ "token": token })), 380 ) 381 .await?; 382 383 let parts: Vec<_> = token.split(".").collect(); 384 if parts.len() != 3 { 385 return Ok(()); 386 } 387 let Ok(payload_str) = String::from_utf8(BASE64_STANDARD_NO_PAD.decode(parts[1]).unwrap()) 388 else { 389 return Ok(()); 390 }; 391 let Ok(payload) = serde_json::from_str::<JsonObject>(&payload_str) else { 392 return Ok(()); 393 }; 394 395 { 396 let mut inner = self.client.inner.write().unwrap(); 397 if let Some(auth) = inner.auth_store.as_mut() { 398 if !auth 399 .record 400 .data 401 .get("verified") 402 .map(|v| v.as_bool().unwrap_or(false)) 403 .unwrap_or(false) 404 && auth.record.data.get("id") == payload.get("id") 405 && auth.record.data.get("collectionId") == payload.get("collectionId") 406 { 407 let _ = auth.record.data.insert("verified".to_string(), true.into()); 408 } 409 } 410 } 411 Ok(()) 412 } 413 414 /// Creates a new item. 415 pub fn create(&self) -> CreateRequestBuilder<'a, JsonObject> { 416 CreateRequestBuilder { 417 record_service: self.clone(), 418 body: Map::new(), 419 } 420 } 421 422 /// Deletes a single record model by its id. 423 pub fn delete(&self, id: &'a str) -> DeleteRequestBuilder<'a> { 424 DeleteRequestBuilder { 425 record_service: self.clone(), 426 id, 427 } 428 } 429 430 /// Returns a list with all items batch fetched at once. 431 pub fn get_full_list(&self) -> FullListRequestBuilder<'a> { 432 FullListRequestBuilder { 433 record_service: self.clone(), 434 batch: None, 435 filter_records: None, 436 order_by: None, 437 expand_fields: None, 438 filter_fields: None, 439 skip_total: None, 440 } 441 } 442 443 /// Returns paginated items list. 444 pub fn get_list(&self, page: usize, per_page: usize) -> ListRequestBuilder<'a> { 445 ListRequestBuilder { 446 record_service: self.clone(), 447 page, 448 per_page, 449 filter_records: None, 450 sort: None, 451 expand_fields: None, 452 filter_fields: None, 453 skip_total: None, 454 } 455 } 456 457 /// Returns single item by its id. 458 /// 459 /// Throws `404` [`Error`] in case an empty id is provided. 460 pub fn get_one(&'a self, id: &'a str) -> ViewRequestBuilder<'a> { 461 ViewRequestBuilder { 462 record_service: self.clone(), 463 id, 464 expand_fields: None, 465 filter_fields: None, 466 } 467 } 468 469 /// Authenticates with the specified recordId and returns a new client with 470 /// the received auth token in a memory store. 471 /// 472 /// This action currently requires superusers privileges. 473 pub fn impersonate(&'a self, id: &'a str) -> ImpersonateBuilder<'a> { 474 ImpersonateBuilder { 475 record_service: self.clone(), 476 id, 477 duration: None, 478 expand_fields: None, 479 filter_fields: None, 480 } 481 } 482 483 /// Returns all available application auth methods. 484 pub async fn list_auth_methods(&'a self) -> Result<AuthMethodList, Error> { 485 self.client 486 .send( 487 &format!("/api/collections/{}/auth-methods", self.collection), 488 Method::GET, 489 HeaderMap::new(), 490 NO_PARAMS, 491 None::<&()>, 492 ) 493 .await 494 } 495 496 /// Sends auth record email change request to the provided email. 497 pub async fn request_email_change(&self, new_email: &str) -> Result<(), Error> { 498 self.client 499 .send( 500 &format!("/api/collections/{}/request-email-change", self.collection), 501 Method::POST, 502 HeaderMap::new(), 503 NO_PARAMS, 504 Some(&json!({ "newEmail": new_email })), 505 ) 506 .await 507 } 508 509 /// Sends auth record OTP request to the provided email. 510 pub async fn request_otp(&self, email: &str) -> Result<String, Error> { 511 #[derive(Deserialize)] 512 #[serde(rename_all = "camelCase")] 513 struct OtpIdResponse { 514 otp_id: String, 515 } 516 517 let response: OtpIdResponse = self 518 .client 519 .send( 520 &format!("/api/collections/{}/request-otp", self.collection), 521 Method::POST, 522 HeaderMap::new(), 523 NO_PARAMS, 524 Some(&json!({ "email": email })), 525 ) 526 .await?; 527 528 Ok(response.otp_id) 529 } 530 531 /// Sends auth record password reset request. 532 pub async fn request_password_reset(&self, email: &str) -> Result<(), Error> { 533 self.client 534 .send( 535 &format!( 536 "/api/collections/{}/request-password-reset", 537 self.collection 538 ), 539 Method::POST, 540 HeaderMap::new(), 541 NO_PARAMS, 542 Some(&json!({ "email": email })), 543 ) 544 .await 545 } 546 547 /// Sends auth record verification email request. 548 pub async fn request_verification(&self, email: &str) -> Result<String, Error> { 549 self.client 550 .send( 551 &format!("/api/collections/{}/request-verification", self.collection), 552 Method::POST, 553 HeaderMap::new(), 554 NO_PARAMS, 555 Some(&json!({ "email": email })), 556 ) 557 .await 558 } 559 560 /// Updates a single record model by its id. 561 pub fn update(&self, id: &'a str) -> UpdateRequestBuilder<'a, JsonObject> { 562 UpdateRequestBuilder { 563 record_service: self.clone(), 564 id, 565 expand_fields: None, 566 filter_fields: None, 567 body: Map::new(), 568 } 569 } 570} 571 572impl<T> ResultList<T> { 573 pub fn items(&self) -> &[T] { 574 &self.items 575 } 576 577 pub fn into_vec(self) -> Vec<T> { 578 self.items 579 } 580 581 pub fn page(&self) -> usize { 582 self.page 583 } 584 585 pub fn per_page(&self) -> usize { 586 self.per_page 587 } 588} 589 590impl<T> IntoIterator for ResultList<T> { 591 type Item = T; 592 type IntoIter = std::vec::IntoIter<T>; 593 594 fn into_iter(self) -> Self::IntoIter { 595 self.items.into_iter() 596 } 597} 598 599impl<'a, T> IntoIterator for &'a ResultList<T> { 600 type Item = &'a T; 601 type IntoIter = std::slice::Iter<'a, T>; 602 603 fn into_iter(self) -> Self::IntoIter { 604 self.items.iter() 605 } 606} 607 608impl<'a, T> IntoIterator for &'a mut ResultList<T> { 609 type Item = &'a mut T; 610 type IntoIter = std::slice::IterMut<'a, T>; 611 612 fn into_iter(self) -> Self::IntoIter { 613 self.items.iter_mut() 614 } 615} 616 617impl<'a> ViewRequestBuilder<'a> { 618 pub fn expand_fields(mut self, field_expression: &'a str) -> Self { 619 self.expand_fields = Some(field_expression); 620 self 621 } 622 623 pub fn filter_fields(mut self, field_expression: &'a str) -> Self { 624 self.filter_fields = Some(field_expression); 625 self 626 } 627 628 pub async fn send<T: DeserializeOwned>(self) -> Result<T, Error> { 629 let mut params = Vec::new(); 630 if let Some(expand) = self.expand_fields { 631 params.push(("expand", expand)); 632 } 633 if let Some(fields) = self.filter_fields { 634 params.push(("fields", fields)); 635 } 636 637 self.record_service 638 .client 639 .send( 640 &format!( 641 "/api/collections/{}/records/{}", 642 self.record_service.collection, self.id 643 ), 644 Method::GET, 645 HeaderMap::new(), 646 &params, 647 None::<&()>, 648 ) 649 .await 650 } 651} 652 653impl<'a, T: Serialize> UpdateRequestBuilder<'a, T> { 654 pub fn expand_fields(mut self, field_expression: &'a str) -> Self { 655 self.expand_fields = Some(field_expression); 656 self 657 } 658 659 pub fn filter_fields(mut self, field_expression: &'a str) -> Self { 660 self.filter_fields = Some(field_expression); 661 self 662 } 663 664 pub fn object<X: Serialize>(self, obj: X) -> UpdateRequestBuilder<'a, X> { 665 UpdateRequestBuilder { 666 record_service: self.record_service, 667 body: obj, 668 expand_fields: self.expand_fields, 669 filter_fields: self.filter_fields, 670 id: self.id, 671 } 672 } 673 674 pub async fn send<R: DeserializeOwned>(self) -> Result<R, Error> { 675 let mut params = Vec::new(); 676 if let Some(expand) = self.expand_fields { 677 params.push(("expand", expand)); 678 } 679 if let Some(fields) = self.filter_fields { 680 params.push(("fields", fields)); 681 } 682 683 self.record_service 684 .client 685 .send( 686 &format!( 687 "/api/collections/{}/records/{}", 688 self.record_service.collection, self.id 689 ), 690 Method::PATCH, 691 HeaderMap::new(), 692 &params, 693 Some(&self.body), 694 ) 695 .await 696 // TODO: The following behaviour is also implemented in the Dart SDK. 697 // 698 // If the current [`AuthStore.record`] matches with the updated id, then on 699 // success the client [`AuthStore`] will be updated with the result model. 700 } 701} 702 703impl<T: Extend<(String, Value)> + Serialize> UpdateRequestBuilder<'_, T> { 704 pub fn fields<Iter: IntoIterator<Item = (String, Value)>>(mut self, patch: Iter) -> Self { 705 self.body.extend(patch); 706 self 707 } 708}