use base64::{Engine, prelude::BASE64_STANDARD_NO_PAD}; use reqwest::{Method, header::HeaderMap}; use serde::{Deserialize, Serialize, de::DeserializeOwned}; use serde_json::{Map, Value, json}; use crate::{ Error, JsonObject, PocketBase, auth::{ AuthMethodList, AuthRefreshBuilder, AuthWithOtpBuilder, AuthWithPasswordBuilder, ImpersonateBuilder, }, }; const NO_PARAMS: &[&str] = &[]; pub struct CreateRequestBuilder<'a, T: Serialize> { record_service: RecordService<'a>, body: T, } pub struct DeleteRequestBuilder<'a> { record_service: RecordService<'a>, id: &'a str, } #[derive(Clone, Debug)] pub struct ListRequestBuilder<'a> { record_service: RecordService<'a>, expand_fields: Option<&'a str>, filter_fields: Option<&'a str>, filter_records: Option<&'a str>, page: usize, per_page: usize, skip_total: Option, sort: Option<&'a str>, } #[derive(Clone, Debug)] pub struct FullListRequestBuilder<'a> { record_service: RecordService<'a>, batch: Option, expand_fields: Option<&'a str>, filter_fields: Option<&'a str>, filter_records: Option<&'a str>, skip_total: Option, order_by: Option<&'a str>, } #[derive(Clone, Debug)] pub struct RecordService<'a> { pub(super) client: &'a PocketBase, pub(super) collection: &'a str, } #[derive(Clone, Debug, Default, Deserialize, Eq, Serialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ResultList { items: Vec, page: usize, per_page: usize, total_items: isize, total_pages: isize, } #[derive(Clone, Debug)] pub struct ViewRequestBuilder<'a> { record_service: RecordService<'a>, expand_fields: Option<&'a str>, filter_fields: Option<&'a str>, id: &'a str, } #[derive(Clone, Debug)] pub struct UpdateRequestBuilder<'a, T: Serialize> { record_service: RecordService<'a>, body: T, expand_fields: Option<&'a str>, filter_fields: Option<&'a str>, id: &'a str, } impl<'a, T: Serialize + Clone> CreateRequestBuilder<'a, T> { pub fn object(self, obj: X) -> CreateRequestBuilder<'a, X> { CreateRequestBuilder { record_service: self.record_service, body: obj, } } pub async fn send(self) -> Result { let params: &[()] = &[]; self.record_service .client .send( &format!( "/api/collections/{}/records", self.record_service.collection ), Method::POST, HeaderMap::new(), ¶ms, Some(&self.body), ) .await } } impl + Serialize> CreateRequestBuilder<'_, T> { pub fn fields>(mut self, patch: Iter) -> Self { self.body.extend(patch); self } } impl DeleteRequestBuilder<'_> { pub async fn send(self) -> Result { let params: &[()] = &[]; self.record_service .client .send( &format!( "/api/collections/{}/records/{}", self.record_service.collection, self.id ), Method::DELETE, HeaderMap::new(), ¶ms, None::<&()>, ) .await // TODO: Dart SDK also implements the following behaviour. // // If the current [`AuthStore.record`] matches with the deleted id, then on // success the client [`AuthStore`] will be also cleared. } } impl<'a> FullListRequestBuilder<'a> { pub fn batch_size(mut self, batch: usize) -> Self { self.batch = Some(batch); self } pub fn expand_fields(mut self, field_expression: &'a str) -> Self { self.expand_fields = Some(field_expression); self } pub fn filter_fields(mut self, field_expression: &'a str) -> Self { self.filter_fields = Some(field_expression); self } pub fn filter_records(mut self, filter_expression: &'a str) -> Self { self.filter_records = Some(filter_expression); self } pub fn skip_total(mut self, skip_total: bool) -> Self { self.skip_total = Some(skip_total); self } pub fn order_by(mut self, sort_expression: &'a str) -> Self { self.order_by = Some(sort_expression); self } pub async fn send(self) -> Result, Error> { let mut list = ListRequestBuilder { record_service: self.record_service, expand_fields: self.expand_fields, filter_fields: self.filter_fields, filter_records: self.filter_records, page: 1, per_page: self.batch.unwrap_or(500), skip_total: Some(true), sort: self.order_by, }; let mut ret = Vec::new(); loop { let page = list.clone().send().await?; let persist = page.items.len() == list.per_page; ret.extend(page.items); if !persist { break; } list.page += 1; } Ok(ret) } } impl<'a> ListRequestBuilder<'a> { pub fn expand_fields(mut self, field_expression: &'a str) -> Self { self.expand_fields = Some(field_expression); self } pub fn filter_fields(mut self, field_expression: &'a str) -> Self { self.filter_fields = Some(field_expression); self } pub fn filter(mut self, filter: &'a str) -> Self { self.filter_records = Some(filter); self } pub fn skip_total(mut self, skip_total: bool) -> Self { self.skip_total = Some(skip_total); self } pub fn sort(mut self, sort: &'a str) -> Self { self.sort = Some(sort); self } pub async fn send(self) -> Result, Error> { let mut storage = vec![self.page.to_string(), self.per_page.to_string()]; if let Some(skip_total) = self.skip_total { storage.push(skip_total.to_string()); } let mut params: Vec<(&str, &str)> = vec![("page", &storage[0]), ("perPage", &storage[1])]; if let Some(filter) = self.filter_records { params.push(("filter", filter)); } if let Some(sort) = self.sort { params.push(("sort", sort)); } if let Some(expand) = self.expand_fields { params.push(("expand", expand)); } if let Some(fields) = self.filter_fields { params.push(("fields", fields)); } if self.skip_total.is_some() { params.push(("skipTotal", &storage[2])); } self.record_service .client .send( &format!( "/api/collections/{}/records", self.record_service.collection ), Method::GET, HeaderMap::new(), ¶ms, None::<&()>, ) .await } } impl<'a> RecordService<'a> { /// Refreshes the current authenticated auth record instance and returns a new /// token and record data. /// /// On success this method automatically updates the client's AuthStore. pub fn auth_refresh(&self) -> AuthRefreshBuilder<'a> { AuthRefreshBuilder { record_service: self.clone(), expand_fields: None, filter_fields: None, } } /// Authenticate an auth record via OTP. /// /// On success this method automatically updates the client's [`AuthStore`]. pub fn auth_with_otp(&'a self, otp_id: &'a str, password: &'a str) -> AuthWithOtpBuilder<'a> { AuthWithOtpBuilder { record_service: self.clone(), otp_id, password, expand_fields: None, filter_fields: None, } } /// Authenticate an auth record by its username/email and password and returns /// a new auth token and record data. /// /// On success this method automatically updates the client's [`AuthStore`]. pub fn auth_with_password( &self, identity: &'a str, password: &'a str, ) -> AuthWithPasswordBuilder<'a> { AuthWithPasswordBuilder { record_service: self.clone(), identity, password, expand_fields: None, filter_fields: None, } } /// Confirms auth record new email address. /// /// If the current [`AuthStore.record`] matches with the record from the /// token, then on success the client [`AuthStore`] will be also cleared. pub async fn confirm_email_change(&self, token: &str, password: &str) -> Result<(), Error> { let () = self .client .send( &format!("/api/collections/{}/confirm-email-change", self.collection), Method::POST, HeaderMap::new(), NO_PARAMS, Some(&json!({ "token": token, "password": password })), ) .await?; let parts: Vec<_> = token.split(".").collect(); if parts.len() != 3 { return Ok(()); } let Ok(payload_str) = String::from_utf8(BASE64_STANDARD_NO_PAD.decode(parts[1]).unwrap()) else { return Ok(()); }; let Ok(payload) = serde_json::from_str::(&payload_str) else { return Ok(()); }; { let inner = self.client.inner.read().unwrap(); if let Some(auth) = inner.auth_store.as_ref() { if auth.record.data.get("id") == payload.get("id") && auth.record.data.get("collectionId") == payload.get("collectionId") { self.client.auth_clear(); } } } Ok(()) } /// Confirms auth record password reset request. pub async fn confirm_password_reset( &self, token: &str, password: &str, password_confirm: &str, ) -> Result<(), Error> { self.client .send( &format!( "/api/collections/{}/confirm-password-reset", self.collection ), Method::POST, HeaderMap::new(), NO_PARAMS, Some( &json!({ "token": token, "password": password, "passwordConfirm": password_confirm }), ), ) .await } /// Confirms auth record email verification request. /// /// On success this method automatically updates the client's [`AuthStore`]. pub async fn confirm_verification(&self, token: &str) -> Result<(), Error> { let () = self .client .send( &format!("/api/collections/{}/confirm-verification", self.collection), Method::POST, HeaderMap::new(), NO_PARAMS, Some(&json!({ "token": token })), ) .await?; let parts: Vec<_> = token.split(".").collect(); if parts.len() != 3 { return Ok(()); } let Ok(payload_str) = String::from_utf8(BASE64_STANDARD_NO_PAD.decode(parts[1]).unwrap()) else { return Ok(()); }; let Ok(payload) = serde_json::from_str::(&payload_str) else { return Ok(()); }; { let mut inner = self.client.inner.write().unwrap(); if let Some(auth) = inner.auth_store.as_mut() { if !auth .record .data .get("verified") .map(|v| v.as_bool().unwrap_or(false)) .unwrap_or(false) && auth.record.data.get("id") == payload.get("id") && auth.record.data.get("collectionId") == payload.get("collectionId") { let _ = auth.record.data.insert("verified".to_string(), true.into()); } } } Ok(()) } /// Creates a new item. pub fn create(&self) -> CreateRequestBuilder<'a, JsonObject> { CreateRequestBuilder { record_service: self.clone(), body: Map::new(), } } /// Deletes a single record model by its id. pub fn delete(&self, id: &'a str) -> DeleteRequestBuilder<'a> { DeleteRequestBuilder { record_service: self.clone(), id, } } /// Returns a list with all items batch fetched at once. pub fn get_full_list(&self) -> FullListRequestBuilder<'a> { FullListRequestBuilder { record_service: self.clone(), batch: None, filter_records: None, order_by: None, expand_fields: None, filter_fields: None, skip_total: None, } } /// Returns paginated items list. pub fn get_list(&self, page: usize, per_page: usize) -> ListRequestBuilder<'a> { ListRequestBuilder { record_service: self.clone(), page, per_page, filter_records: None, sort: None, expand_fields: None, filter_fields: None, skip_total: None, } } /// Returns single item by its id. /// /// Throws `404` [`Error`] in case an empty id is provided. pub fn get_one(&'a self, id: &'a str) -> ViewRequestBuilder<'a> { ViewRequestBuilder { record_service: self.clone(), id, expand_fields: None, filter_fields: None, } } /// Authenticates with the specified recordId and returns a new client with /// the received auth token in a memory store. /// /// This action currently requires superusers privileges. pub fn impersonate(&'a self, id: &'a str) -> ImpersonateBuilder<'a> { ImpersonateBuilder { record_service: self.clone(), id, duration: None, expand_fields: None, filter_fields: None, } } /// Returns all available application auth methods. pub async fn list_auth_methods(&'a self) -> Result { self.client .send( &format!("/api/collections/{}/auth-methods", self.collection), Method::GET, HeaderMap::new(), NO_PARAMS, None::<&()>, ) .await } /// Sends auth record email change request to the provided email. pub async fn request_email_change(&self, new_email: &str) -> Result<(), Error> { self.client .send( &format!("/api/collections/{}/request-email-change", self.collection), Method::POST, HeaderMap::new(), NO_PARAMS, Some(&json!({ "newEmail": new_email })), ) .await } /// Sends auth record OTP request to the provided email. pub async fn request_otp(&self, email: &str) -> Result { #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct OtpIdResponse { otp_id: String, } let response: OtpIdResponse = self .client .send( &format!("/api/collections/{}/request-otp", self.collection), Method::POST, HeaderMap::new(), NO_PARAMS, Some(&json!({ "email": email })), ) .await?; Ok(response.otp_id) } /// Sends auth record password reset request. pub async fn request_password_reset(&self, email: &str) -> Result<(), Error> { self.client .send( &format!( "/api/collections/{}/request-password-reset", self.collection ), Method::POST, HeaderMap::new(), NO_PARAMS, Some(&json!({ "email": email })), ) .await } /// Sends auth record verification email request. pub async fn request_verification(&self, email: &str) -> Result { self.client .send( &format!("/api/collections/{}/request-verification", self.collection), Method::POST, HeaderMap::new(), NO_PARAMS, Some(&json!({ "email": email })), ) .await } /// Updates a single record model by its id. pub fn update(&self, id: &'a str) -> UpdateRequestBuilder<'a, JsonObject> { UpdateRequestBuilder { record_service: self.clone(), id, expand_fields: None, filter_fields: None, body: Map::new(), } } } impl ResultList { pub fn items(&self) -> &[T] { &self.items } pub fn into_vec(self) -> Vec { self.items } pub fn page(&self) -> usize { self.page } pub fn per_page(&self) -> usize { self.per_page } } impl IntoIterator for ResultList { type Item = T; type IntoIter = std::vec::IntoIter; fn into_iter(self) -> Self::IntoIter { self.items.into_iter() } } impl<'a, T> IntoIterator for &'a ResultList { type Item = &'a T; type IntoIter = std::slice::Iter<'a, T>; fn into_iter(self) -> Self::IntoIter { self.items.iter() } } impl<'a, T> IntoIterator for &'a mut ResultList { type Item = &'a mut T; type IntoIter = std::slice::IterMut<'a, T>; fn into_iter(self) -> Self::IntoIter { self.items.iter_mut() } } impl<'a> ViewRequestBuilder<'a> { pub fn expand_fields(mut self, field_expression: &'a str) -> Self { self.expand_fields = Some(field_expression); self } pub fn filter_fields(mut self, field_expression: &'a str) -> Self { self.filter_fields = Some(field_expression); self } pub async fn send(self) -> Result { let mut params = Vec::new(); if let Some(expand) = self.expand_fields { params.push(("expand", expand)); } if let Some(fields) = self.filter_fields { params.push(("fields", fields)); } self.record_service .client .send( &format!( "/api/collections/{}/records/{}", self.record_service.collection, self.id ), Method::GET, HeaderMap::new(), ¶ms, None::<&()>, ) .await } } impl<'a, T: Serialize> UpdateRequestBuilder<'a, T> { pub fn expand_fields(mut self, field_expression: &'a str) -> Self { self.expand_fields = Some(field_expression); self } pub fn filter_fields(mut self, field_expression: &'a str) -> Self { self.filter_fields = Some(field_expression); self } pub fn object(self, obj: X) -> UpdateRequestBuilder<'a, X> { UpdateRequestBuilder { record_service: self.record_service, body: obj, expand_fields: self.expand_fields, filter_fields: self.filter_fields, id: self.id, } } pub async fn send(self) -> Result { let mut params = Vec::new(); if let Some(expand) = self.expand_fields { params.push(("expand", expand)); } if let Some(fields) = self.filter_fields { params.push(("fields", fields)); } self.record_service .client .send( &format!( "/api/collections/{}/records/{}", self.record_service.collection, self.id ), Method::PATCH, HeaderMap::new(), ¶ms, Some(&self.body), ) .await // TODO: The following behaviour is also implemented in the Dart SDK. // // If the current [`AuthStore.record`] matches with the updated id, then on // success the client [`AuthStore`] will be updated with the result model. } } impl + Serialize> UpdateRequestBuilder<'_, T> { pub fn fields>(mut self, patch: Iter) -> Self { self.body.extend(patch); self } }