···55use crate::{Error, PocketBase};
6677#[derive(Clone, Debug)]
88+/// Service that handles the Health APIs.
89pub struct HealthService<'a> {
1010+ /// Reference to the PocketBase client.
911 pub(super) client: &'a PocketBase,
1212+ /// Optional comma-separated list of fields to include in the response.
1013 pub(super) fields: Option<String>,
1114}
12151316#[derive(Clone, Debug, Deserialize)]
1417pub struct HealthCheck {
1818+ /// Status message.
1519 pub message: String,
2020+ /// Additional data.
1621 pub data: Map<String, Value>,
1722}
18231924impl HealthService<'_> {
2525+ /// Checks the health status of the api.
2026 pub async fn check(self) -> Result<HealthCheck, Error> {
2127 let mut params = Vec::new();
2228 if let Some(fields) = self.fields {
+31-5
src/lib.rs
···77use serde::{Serialize, de::DeserializeOwned};
88use serde_json::{Map, Value};
99use thiserror::Error;
1010-use url::{ParseError, Url};
1010+use url::Url;
11111212pub mod auth;
1313pub mod health;
···15151616use crate::{auth::AuthStore, health::HealthService, record::RecordService};
17171818-pub type Error = reqwest::Error;
1818+/// Type alias for reqwest's error type, that should be generally used if an
1919+/// error is returned.
2020+type Error = reqwest::Error;
19212020-pub type JsonObject = Map<String, Value>;
2222+/// Type alias for a type that can represent general JSON objects.
2323+type JsonObject = Map<String, Value>;
21242225#[derive(Clone, Debug)]
2626+/// Main PocketBase API client.
2327pub struct PocketBase {
2428 inner: Arc<RwLock<PocketBaseInner>>,
2529}
26302731#[derive(Clone, Debug)]
3232+/// Inner storage for the PocketBase API client.
2833struct PocketBaseInner {
2929- reqwest: Client,
3434+ /// Storage of the authenticated user and the authenticated token, if any.
3035 auth_store: Option<AuthStore>,
3636+ /// The PocketBase backend base url address (e.g. `http://127.0.0.1:8090`).
3137 base_url: Url,
3838+ /// Language code that will be sent with the requests to the server as
3939+ /// `Accept-Language` header.
4040+ ///
4141+ /// Will be set to `en-US` by default.
3242 lang: String,
4343+ /// Reqwest [`Client`] used.
4444+ reqwest: Client,
3345}
34463547impl PocketBase {
4848+ /// Clears the previously stored token and record auth data.
3649 pub fn auth_clear<'a>(&'a self) {
3750 let mut write = self.inner.write().unwrap();
3851 write.auth_store = None;
3952 }
40535454+ /// Returns the [`RecordService`] associated to the specified collection.
4155 pub fn collection<'a>(&'a self, collection: &'a str) -> RecordService<'a> {
4256 RecordService {
4357 client: self,
···4559 }
4660 }
47616262+ /// Return an instance of the service that handles the Health APIs.
4863 pub fn health(&self) -> HealthService {
4964 HealthService {
5065 client: self,
···5267 }
5368 }
54697070+ /// Get the language code that will be sent with the requests to the server as
7171+ /// `Accept-Language` header.
5572 pub fn lang(&self) -> String {
5673 self.inner.read().unwrap().lang.clone()
5774 }
58755959- pub fn new(base_url: &str) -> Result<Self, ParseError> {
7676+ /// Create a new [`PocketBase`] instance using the provided base URL for the
7777+ /// PocketBase instance (e.g. `http://127.0.0.1:8090`).
7878+ pub fn new(base_url: &str) -> Result<Self, url::ParseError> {
6079 let base_url = Url::parse(base_url)?;
6180 let inner = PocketBaseInner {
6281 reqwest: Client::new(),
···6988 })
7089 }
71909191+ /// Internal method used to send requests to the Pockatbase instance.
7292 async fn send<Q, B, T>(
7393 &self,
7494 path: &str,
···125145 response.error_for_status()?.json().await
126146 }
127147148148+ /// Internal method to set the authentication store when using one of the
149149+ /// authentication methods.
128150 fn with_auth_store(&self, auth: AuthStore) {
129151 let mut write = self.inner.write().unwrap();
130152 write.auth_store = Some(auth);
131153 }
132154155155+ /// Set the language code that will be sent with the requests to the server as
156156+ /// `Accept-Language` header.
157157+ ///
158158+ /// Will be`en-US`, if not set.
133159 pub fn with_lang(self, lang: String) -> Self {
134160 {
135161 let mut write = self.inner.write().unwrap();
+252-8
src/record.rs
···11+use base64::{Engine, prelude::BASE64_STANDARD_NO_PAD};
12use reqwest::{Method, header::HeaderMap};
23use serde::{Deserialize, Serialize, de::DeserializeOwned};
33-use serde_json::{Map, Value};
44+use serde_json::{Map, Value, json};
4556use crate::{
67 Error, JsonObject, PocketBase,
77- auth::{AuthRefreshBuilder, AuthWithPasswordBuilder},
88+ auth::{
99+ AuthMethodList, AuthRefreshBuilder, AuthWithOtpBuilder, AuthWithPasswordBuilder,
1010+ ImpersonateBuilder,
1111+ },
812};
9131010-pub struct CreateRequestBuilder<'a, T: Serialize = JsonObject> {
1414+const NO_PARAMS: &[&str] = &[];
1515+1616+pub struct CreateRequestBuilder<'a, T: Serialize> {
1117 record_service: RecordService<'a>,
1218 body: T,
1319}
···6571}
66726773#[derive(Clone, Debug)]
6868-pub struct UpdateRequestBuilder<'a, T: Serialize = JsonObject> {
7474+pub struct UpdateRequestBuilder<'a, T: Serialize> {
6975 record_service: RecordService<'a>,
7076 body: T,
7177 expand_fields: Option<&'a str>,
···100106 }
101107}
102108103103-impl CreateRequestBuilder<'_, JsonObject> {
109109+impl<T: Extend<(String, Value)> + Serialize> CreateRequestBuilder<'_, T> {
104110 pub fn fields<Iter: IntoIterator<Item = (String, Value)>>(mut self, patch: Iter) -> Self {
105111 self.body.extend(patch);
106112 self
···123129 None::<&()>,
124130 )
125131 .await
132132+ // TODO: Dart SDK also implements the following behaviour.
133133+ //
134134+ // If the current [`AuthStore.record`] matches with the deleted id, then on
135135+ // success the client [`AuthStore`] will be also cleared.
126136 }
127137}
128138···248258}
249259250260impl<'a> RecordService<'a> {
261261+ /// Refreshes the current authenticated auth record instance and returns a new
262262+ /// token and record data.
263263+ ///
264264+ /// On success this method automatically updates the client's AuthStore.
251265 pub fn auth_refresh(&self) -> AuthRefreshBuilder<'a> {
252266 AuthRefreshBuilder {
253267 record_service: self.clone(),
···256270 }
257271 }
258272273273+ /// Authenticate an auth record via OTP.
274274+ ///
275275+ /// On success this method automatically updates the client's [`AuthStore`].
276276+ pub fn auth_with_otp(&'a self, otp_id: &'a str, password: &'a str) -> AuthWithOtpBuilder<'a> {
277277+ AuthWithOtpBuilder {
278278+ record_service: self.clone(),
279279+ otp_id,
280280+ password,
281281+ expand_fields: None,
282282+ filter_fields: None,
283283+ }
284284+ }
285285+286286+ /// Authenticate an auth record by its username/email and password and returns
287287+ /// a new auth token and record data.
288288+ ///
289289+ /// On success this method automatically updates the client's [`AuthStore`].
259290 pub fn auth_with_password(
260291 &self,
261292 identity: &'a str,
···270301 }
271302 }
272303273273- pub fn create(&self) -> CreateRequestBuilder<'a> {
304304+ /// Confirms auth record new email address.
305305+ ///
306306+ /// If the current [`AuthStore.record`] matches with the record from the
307307+ /// token, then on success the client [`AuthStore`] will be also cleared.
308308+ pub async fn confirm_email_change(&self, token: &str, password: &str) -> Result<(), Error> {
309309+ let () = self
310310+ .client
311311+ .send(
312312+ &format!("/api/collections/{}/confirm-email-change", self.collection),
313313+ Method::POST,
314314+ HeaderMap::new(),
315315+ NO_PARAMS,
316316+ Some(&json!({ "token": token, "password": password })),
317317+ )
318318+ .await?;
319319+320320+ let parts: Vec<_> = token.split(".").collect();
321321+ if parts.len() != 3 {
322322+ return Ok(());
323323+ }
324324+ let Ok(payload_str) = String::from_utf8(BASE64_STANDARD_NO_PAD.decode(parts[1]).unwrap())
325325+ else {
326326+ return Ok(());
327327+ };
328328+ let Ok(payload) = serde_json::from_str::<JsonObject>(&payload_str) else {
329329+ return Ok(());
330330+ };
331331+332332+ {
333333+ let inner = self.client.inner.read().unwrap();
334334+ if let Some(auth) = inner.auth_store.as_ref() {
335335+ if auth.record.data.get("id") == payload.get("id")
336336+ && auth.record.data.get("collectionId") == payload.get("collectionId")
337337+ {
338338+ self.client.auth_clear();
339339+ }
340340+ }
341341+ }
342342+ Ok(())
343343+ }
344344+345345+ /// Confirms auth record password reset request.
346346+ pub async fn confirm_password_reset(
347347+ &self,
348348+ token: &str,
349349+ password: &str,
350350+ password_confirm: &str,
351351+ ) -> Result<(), Error> {
352352+ self.client
353353+ .send(
354354+ &format!(
355355+ "/api/collections/{}/confirm-password-reset",
356356+ self.collection
357357+ ),
358358+ Method::POST,
359359+ HeaderMap::new(),
360360+ NO_PARAMS,
361361+ Some(
362362+ &json!({ "token": token, "password": password, "passwordConfirm": password_confirm }),
363363+ ),
364364+ )
365365+ .await
366366+ }
367367+368368+ /// Confirms auth record email verification request.
369369+ ///
370370+ /// On success this method automatically updates the client's [`AuthStore`].
371371+ pub async fn confirm_verification(&self, token: &str) -> Result<(), Error> {
372372+ let () = self
373373+ .client
374374+ .send(
375375+ &format!("/api/collections/{}/confirm-verification", self.collection),
376376+ Method::POST,
377377+ HeaderMap::new(),
378378+ NO_PARAMS,
379379+ Some(&json!({ "token": token })),
380380+ )
381381+ .await?;
382382+383383+ let parts: Vec<_> = token.split(".").collect();
384384+ if parts.len() != 3 {
385385+ return Ok(());
386386+ }
387387+ let Ok(payload_str) = String::from_utf8(BASE64_STANDARD_NO_PAD.decode(parts[1]).unwrap())
388388+ else {
389389+ return Ok(());
390390+ };
391391+ let Ok(payload) = serde_json::from_str::<JsonObject>(&payload_str) else {
392392+ return Ok(());
393393+ };
394394+395395+ {
396396+ let mut inner = self.client.inner.write().unwrap();
397397+ if let Some(auth) = inner.auth_store.as_mut() {
398398+ if !auth
399399+ .record
400400+ .data
401401+ .get("verified")
402402+ .map(|v| v.as_bool().unwrap_or(false))
403403+ .unwrap_or(false)
404404+ && auth.record.data.get("id") == payload.get("id")
405405+ && auth.record.data.get("collectionId") == payload.get("collectionId")
406406+ {
407407+ let _ = auth.record.data.insert("verified".to_string(), true.into());
408408+ }
409409+ }
410410+ }
411411+ Ok(())
412412+ }
413413+414414+ /// Creates a new item.
415415+ pub fn create(&self) -> CreateRequestBuilder<'a, JsonObject> {
274416 CreateRequestBuilder {
275417 record_service: self.clone(),
276418 body: Map::new(),
277419 }
278420 }
279421422422+ /// Deletes a single record model by its id.
280423 pub fn delete(&self, id: &'a str) -> DeleteRequestBuilder<'a> {
281424 DeleteRequestBuilder {
282425 record_service: self.clone(),
···284427 }
285428 }
286429430430+ /// Returns a list with all items batch fetched at once.
287431 pub fn get_full_list(&self) -> FullListRequestBuilder<'a> {
288432 FullListRequestBuilder {
289433 record_service: self.clone(),
···296440 }
297441 }
298442443443+ /// Returns paginated items list.
299444 pub fn get_list(&self, page: usize, per_page: usize) -> ListRequestBuilder<'a> {
300445 ListRequestBuilder {
301446 record_service: self.clone(),
···309454 }
310455 }
311456457457+ /// Returns single item by its id.
458458+ ///
459459+ /// Throws `404` [`Error`] in case an empty id is provided.
312460 pub fn get_one(&'a self, id: &'a str) -> ViewRequestBuilder<'a> {
313461 ViewRequestBuilder {
314462 record_service: self.clone(),
···318466 }
319467 }
320468321321- pub fn update(&self, id: &'a str) -> UpdateRequestBuilder<'a> {
469469+ /// Authenticates with the specified recordId and returns a new client with
470470+ /// the received auth token in a memory store.
471471+ ///
472472+ /// This action currently requires superusers privileges.
473473+ pub fn impersonate(&'a self, id: &'a str) -> ImpersonateBuilder<'a> {
474474+ ImpersonateBuilder {
475475+ record_service: self.clone(),
476476+ id,
477477+ duration: None,
478478+ expand_fields: None,
479479+ filter_fields: None,
480480+ }
481481+ }
482482+483483+ /// Returns all available application auth methods.
484484+ pub async fn list_auth_methods(&'a self) -> Result<AuthMethodList, Error> {
485485+ self.client
486486+ .send(
487487+ &format!("/api/collections/{}/auth-methods", self.collection),
488488+ Method::GET,
489489+ HeaderMap::new(),
490490+ NO_PARAMS,
491491+ None::<&()>,
492492+ )
493493+ .await
494494+ }
495495+496496+ /// Sends auth record email change request to the provided email.
497497+ pub async fn request_email_change(&self, new_email: &str) -> Result<(), Error> {
498498+ self.client
499499+ .send(
500500+ &format!("/api/collections/{}/request-email-change", self.collection),
501501+ Method::POST,
502502+ HeaderMap::new(),
503503+ NO_PARAMS,
504504+ Some(&json!({ "newEmail": new_email })),
505505+ )
506506+ .await
507507+ }
508508+509509+ /// Sends auth record OTP request to the provided email.
510510+ pub async fn request_otp(&self, email: &str) -> Result<String, Error> {
511511+ #[derive(Deserialize)]
512512+ #[serde(rename_all = "camelCase")]
513513+ struct OtpIdResponse {
514514+ otp_id: String,
515515+ }
516516+517517+ let response: OtpIdResponse = self
518518+ .client
519519+ .send(
520520+ &format!("/api/collections/{}/request-otp", self.collection),
521521+ Method::POST,
522522+ HeaderMap::new(),
523523+ NO_PARAMS,
524524+ Some(&json!({ "email": email })),
525525+ )
526526+ .await?;
527527+528528+ Ok(response.otp_id)
529529+ }
530530+531531+ /// Sends auth record password reset request.
532532+ pub async fn request_password_reset(&self, email: &str) -> Result<(), Error> {
533533+ self.client
534534+ .send(
535535+ &format!(
536536+ "/api/collections/{}/request-password-reset",
537537+ self.collection
538538+ ),
539539+ Method::POST,
540540+ HeaderMap::new(),
541541+ NO_PARAMS,
542542+ Some(&json!({ "email": email })),
543543+ )
544544+ .await
545545+ }
546546+547547+ /// Sends auth record verification email request.
548548+ pub async fn request_verification(&self, email: &str) -> Result<String, Error> {
549549+ self.client
550550+ .send(
551551+ &format!("/api/collections/{}/request-verification", self.collection),
552552+ Method::POST,
553553+ HeaderMap::new(),
554554+ NO_PARAMS,
555555+ Some(&json!({ "email": email })),
556556+ )
557557+ .await
558558+ }
559559+560560+ /// Updates a single record model by its id.
561561+ pub fn update(&self, id: &'a str) -> UpdateRequestBuilder<'a, JsonObject> {
322562 UpdateRequestBuilder {
323563 record_service: self.clone(),
324564 id,
···453693 Some(&self.body),
454694 )
455695 .await
696696+ // TODO: The following behaviour is also implemented in the Dart SDK.
697697+ //
698698+ // If the current [`AuthStore.record`] matches with the updated id, then on
699699+ // success the client [`AuthStore`] will be updated with the result model.
456700 }
457701}
458702459459-impl UpdateRequestBuilder<'_, JsonObject> {
703703+impl<T: Extend<(String, Value)> + Serialize> UpdateRequestBuilder<'_, T> {
460704 pub fn fields<Iter: IntoIterator<Item = (String, Value)>>(mut self, patch: Iter) -> Self {
461705 self.body.extend(patch);
462706 self