Skip to content

Commit

Permalink
Improved documentation throughout crate
Browse files Browse the repository at this point in the history
  • Loading branch information
strykejern committed Oct 18, 2024
1 parent 3e11bf5 commit ffe3dbd
Show file tree
Hide file tree
Showing 8 changed files with 227 additions and 31 deletions.
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ The goal is to support as much of the Supabase API as possible. Currently, the f
## Platform compatibility

The project supports both the `stable-x86_64-unknown-linux-gnu` and `wasm32-unknown-unknown` targets.
More targets might also work, but WASM is actively targeted for this crate.

## Installation

`cargo add suparust`

## Usage
## Usage examples

```rust
let client = suparust::Supabase::new(
Expand All @@ -58,6 +59,35 @@ let table_contents = client
.execute()
.await?
.json::<Vec<MyStruct> > ();

// Storage example

use suparust::storage::object::*;
let list_request = ListRequest::new("my_folder".to_string())
.limit(10)
.sort_by("my_column", SortOrder::Ascending);
let objects = client
.storage()
.await
.object()
.list("my_bucket", list_request)
.await?;

let object_names = objects
.iter()
.map(|object| object.name.clone());

let mut downloaded_objects = vec![];

for object in objects {
let downloaded = client
.storage()
.await
.object()
.get_one("my_bucket", &object.name)
.await?;
downloaded_objects.push(downloaded);
}
```

More examples will come as the library matures.
Expand Down
39 changes: 33 additions & 6 deletions src/auth.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
use crate::{Result, SessionChangeListener, Supabase, SupabaseError};
use crate::{Result, Supabase, SupabaseError};
use std::sync::Arc;
use supabase_auth::models::{LogoutScope, Session, User};
pub use supabase_auth::models::{LogoutScope, Session, User};
use tokio::sync::RwLock;

pub const SESSION_REFRESH_GRACE_PERIOD_SECONDS: i64 = 60;

pub struct UpdateUserBuilder {
user_info: supabase_auth::models::UpdateUserPayload,
auth: Arc<supabase_auth::models::AuthClient>,
session: Arc<RwLock<Option<Session>>>,
}

/// A listener for changes to a session
#[derive(Clone)]
pub enum SessionChangeListener {
Ignore,
Sync(std::sync::mpsc::Sender<Session>),
Async(tokio::sync::mpsc::Sender<Session>),
}

impl Supabase {
async fn set_auth_state(&self, session: Session) {
*self.session.write().await = Some(session.clone());
Expand All @@ -33,11 +43,16 @@ impl Supabase {
}
}

/// This function can be used to tell if we most likely have session credentials that are valid.
/// One use case is to tell if we are logged in or not.
pub async fn has_valid_auth_state(&self) -> bool {
self.session.read().await.is_some()
}

pub async fn login_with_email(&self, email: &str, password: &str) -> crate::Result<Session> {
/// Login with email and password. If successful, the Supabase object will now use the credentials
/// automatically for all requests. We will also return the session information on success, so that
/// the caller can e.g. save it for later use (e.g. in calls to `new`).
pub async fn login_with_email(&self, email: &str, password: &str) -> Result<Session> {
let session = self.auth.login_with_email(email, password).await?;

self.set_auth_state(session.clone()).await;
Expand All @@ -51,8 +66,9 @@ impl Supabase {
if let Some(auth_state) = auth_state {
let now_epoch = now_as_epoch()?;

// Refresh 1 minute before expiration
let expired = (auth_state.expires_at as i64) < now_epoch + 60;
// Refresh some time before the session expires
let expired =
(auth_state.expires_at as i64) < now_epoch + SESSION_REFRESH_GRACE_PERIOD_SECONDS;

if expired {
match self.auth.refresh_session(auth_state.refresh_token).await {
Expand All @@ -76,7 +92,10 @@ impl Supabase {
}
}

pub async fn logout(&self, scope: Option<LogoutScope>) -> crate::Result<()> {
/// Log out of the current session. This will invalidate the current session in the Supabase server
/// and remove it from this Supabase object. Further uses of this object will then not be
/// authenticated.
pub async fn logout(&self, scope: Option<LogoutScope>) -> Result<()> {
self.refresh_login().await?;

let token = self
Expand All @@ -94,6 +113,7 @@ impl Supabase {
Ok(())
}

/// If logged in, will return the current user information.
pub async fn user(&self) -> Option<User> {
self.session
.read()
Expand All @@ -102,6 +122,8 @@ impl Supabase {
.map(|session| session.user.clone())
}

/// Update the current user. This will return a builder object that can be used to set the different
/// fields applicable.
pub async fn update_user(&self) -> Result<UpdateUserBuilder> {
self.refresh_login().await?;

Expand All @@ -118,6 +140,7 @@ impl Supabase {
}

impl UpdateUserBuilder {
/// Send the update request to the server. This will return the updated user information.
pub async fn send(self) -> Result<User> {
let token = self
.session
Expand All @@ -131,11 +154,15 @@ impl UpdateUserBuilder {
Ok(user)
}

/// Set the email that you want to set your currently logged-in user to have. Remember that the
/// email is not set until you call `send`.
pub fn email<StringType: ToString>(mut self, email: StringType) -> Self {
self.user_info.email = Some(email.to_string());
self
}

/// Set the password that you want to set your currently logged-in user to have. Remember that
/// the password is not set until you call `send`.
pub fn password<StringType: ToString>(mut self, password: StringType) -> Self {
self.user_info.password = Some(password.to_string());
self
Expand Down
2 changes: 1 addition & 1 deletion src/external/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1 @@
pub(crate) mod postgrest_rs;
pub mod postgrest_rs;
158 changes: 139 additions & 19 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,49 @@
//! # Examples
//! # suparust
//!
//! Simple example:
//! ```
//! A crate for interacting with Supabase. Also supports WASM targets.
//!
//! ## Usage
//!
//! Create your Supabase client with `Supabase::new` and start using it. The client will automatically
//! handle authentication for you after you have logged in with the client.
//!
//! ### Postgrest
//!
//! Use the functions [`from`](Supabase::from) and [`rpc`](Supabase::rpc) to get a [`postgrest::Builder`] that
//! you can use to build your queries. The builder will automatically have authentication (if it's available)
//! when it's first created.
//!
//! ### Storage
//!
//! Use the function [`storage`](Supabase::storage) to get a one-time-use [`storage::Storage`] client for interacting
//! with the storage part of Supabase. The client will automatically have authentication (if it's available)
//! when it's first created.
//!
//! ### Auth
//!
//! Auth functions are available directly on the Supabase client. Use the functions [`login_with_email`](Supabase::login_with_email),
//! and [`logout`](Supabase::logout) for basic authentication. The client will automatically handle
//! refreshing if needed when making requests.
//!
//! The session refresh happens if it is less than [`auth::SESSION_REFRESH_GRACE_PERIOD_SECONDS`] seconds
//! from expiring. This means that you should not keep authenticated builders/temporary clients for
//! too long before using them, as they might time out.
//!
//! <div class="warning">
//! Don't keep authenticated builders/clients from postgrest and storage too long, as they might
//! time out after some time. See details in Auth description above.
//! </div>
//!
//! ## Examples
//!
//! ### Simple postgrest example
//! ```no_run
//! # pub async fn run() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
//! let client = suparust::Supabase::new(
//! "https://your.postgrest.endpoint",
//! "your_api_key",
//! None,
//! suparust::SessionChangeListener::Ignore);
//! suparust::auth::SessionChangeListener::Ignore);
//!
//! client.login_with_email(
//! "[email protected]",
Expand All @@ -30,25 +66,68 @@
//!
//! # Ok(())
//! # }
//! ```
//!
//! ### Storage example
//! ```no_run
//! # pub async fn run() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
//! let client = suparust::Supabase::new(
//! "https://your.postgrest.endpoint",
//! "your_api_key",
//! None,
//! suparust::auth::SessionChangeListener::Ignore);
//!
//! // Login here
//!
//! # use suparust::storage::object::*;
//! let list_request = ListRequest::new("my_folder".to_string())
//! .limit(10)
//! .sort_by("my_column", SortOrder::Ascending);
//! let objects = client
//! .storage()
//! .await
//! .object()
//! .list("my_bucket", list_request)
//! .await?;
//!
//! let object_names = objects
//! .iter()
//! .map(|object| object.name.clone());
//!
//! let mut downloaded_objects = vec![];
//!
//! for object in objects {
//! let downloaded = client
//! .storage()
//! .await
//! .object()
//! .get_one("my_bucket", &object.name)
//! .await?;
//! downloaded_objects.push(downloaded);
//! }
//!
//! # Ok(())
//! # }
//! ```
mod auth;
pub mod auth;
mod external;
mod postgrest;
pub mod postgrest;
pub mod storage;
#[cfg(test)]
mod tests;

use std::sync::Arc;
pub use supabase_auth::models::{LogoutScope, Session, User};
use tokio::sync::RwLock;

pub type Result<Type> = std::result::Result<Type, SupabaseError>;

/// The main Supabase client. This is safely cloneable.
#[derive(Clone)]
pub struct Supabase {
auth: Arc<supabase_auth::models::AuthClient>,
session: Arc<RwLock<Option<Session>>>,
session_listener: SessionChangeListener,
session: Arc<RwLock<Option<auth::Session>>>,
session_listener: auth::SessionChangeListener,
postgrest: Arc<RwLock<external::postgrest_rs::Postgrest>>,
storage_client: reqwest::Client,
api_key: String,
Expand All @@ -57,9 +136,11 @@ pub struct Supabase {

#[derive(thiserror::Error, Debug)]
pub enum SupabaseError {
/// Failed to refresh session
#[error("Failed to refresh session: {0}")]
SessionRefresh(supabase_auth::error::Error),
#[error("Missing authentication information")]
/// Missing authentication information. Maybe you are not logged in?
#[error("Missing authentication information. Maybe you are not logged in?")]
MissingAuthenticationInformation,
#[error("Request failed")]
Reqwest(#[from] reqwest::Error),
Expand All @@ -69,19 +150,58 @@ pub enum SupabaseError {
Internal(#[from] Box<dyn std::error::Error + Send + Sync>),
}

#[derive(Clone)]
pub enum SessionChangeListener {
Ignore,
Sync(std::sync::mpsc::Sender<Session>),
Async(tokio::sync::mpsc::Sender<Session>),
}

impl Supabase {
/// Create a new Supabase client
///
/// # Arguments
/// * `url` - The URL of the Postgrest endpoint
/// * `api_key` - The API key for the Postgrest endpoint
/// * `session` - An optional session to use for authentication. This is typically session
/// information that is either gotten through the listener (next parameter to this function),
/// or externally if you get a valid session from somewhere else (e.g. a magic link).
/// * `session_listener` - A listener for session changes. This can be used to listen for session
/// changes and e.g. update a saved state for use at next run. If you don't need this, you
/// can use `SessionChangeListener::Ignore`.
///
/// # Example
///
/// ## Basic usage
/// ```no_run
/// # use suparust::*;
/// let client = Supabase::new(
/// "https://your.postgrest.endpoint",
/// "your_api_key",
/// None,
/// auth::SessionChangeListener::Ignore);
/// ```
///
/// ## Persist session information
/// ```no_run
/// # use suparust::*;
/// # fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
/// # let load_session = || None;
///
/// let loaded_session = load_session(); // Load session from somewhere
///
/// let (sender, receiver) = std::sync::mpsc::channel();
///
/// let client = Supabase::new(
/// "https://your.postgrest.endpoint",
/// "your_api_key",
/// loaded_session,
/// auth::SessionChangeListener::Sync(sender));
///
/// let session = receiver.recv()?;
///
/// # let save_session = | _session | ();
/// save_session(session);
/// # Ok(())
/// # }
pub fn new(
url: &str,
api_key: &str,
session: Option<Session>,
session_listener: SessionChangeListener,
session: Option<auth::Session>,
session_listener: auth::SessionChangeListener,
) -> Self {
let mut postgrest = external::postgrest_rs::Postgrest::new(format!("{url}/rest/v1"))
.insert_header("apikey", api_key);
Expand Down
10 changes: 8 additions & 2 deletions src/postgrest.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
//! Holds some types that you will get from using the Supabase client. These types are not meant to
//! be used directly.
use crate::Result;
use crate::Supabase;

use crate::external::postgrest_rs as postgrest;
pub use postgrest::Builder;

impl Supabase {
pub async fn from<T>(&self, table: T) -> Result<postgrest::Builder>
/// A wrapper for `postgrest::Postgrest::from` that gives you an already authenticated [`Builder`]
pub async fn from<T>(&self, table: T) -> Result<Builder>
where
T: AsRef<str>,
{
Expand All @@ -13,7 +18,8 @@ impl Supabase {
Ok(self.postgrest.read().await.from(table))
}

pub async fn rpc<T, U>(&self, function: T, params: U) -> Result<postgrest::Builder>
/// A wrapper for `postgrest::Postgrest::rpc` that gives you an already authenticated [`Builder`]
pub async fn rpc<T, U>(&self, function: T, params: U) -> Result<Builder>
where
T: AsRef<str>,
U: Into<String>,
Expand Down
Loading

0 comments on commit ffe3dbd

Please sign in to comment.