Installation
Addtrailbase-client to your Cargo.toml:
Cargo.toml
[dependencies]
trailbase-client = "0.5.1"
Features
ws- Enable WebSocket support for subscriptions (optional)
Cargo.toml
[dependencies]
trailbase-client = { version = "0.5.1", features = ["ws"] }
Initialization
Basic Client
use trailbase_client::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new("https://your-server.trailbase.io").await?;
Ok(())
}
Client with Tokens
use trailbase_client::{Client, Tokens};
let tokens = Tokens {
auth_token: "your-auth-token".to_string(),
refresh_token: Some("your-refresh-token".to_string()),
csrf_token: Some("your-csrf-token".to_string()),
};
let client = Client::with_tokens(
"https://your-server.trailbase.io",
tokens
).await?;
Authentication
Login
let tokens = client.login("[email protected]", "password").await?;
println!("Auth token: {}", tokens.auth_token);
let user = client.user();
if let Some(user) = user {
println!("Logged in as: {}", user.email);
}
Logout
client.logout().await?;
Current User
if let Some(user) = client.user() {
println!("User ID: {}", user.sub);
println!("Email: {}", user.email);
}
Access Tokens
if let Some(tokens) = client.tokens() {
// Persist tokens for later use
let json = serde_json::to_string(&tokens)?;
std::fs::write("tokens.json", json)?;
}
Record API
Define Your Record Types
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct Post {
id: String,
title: String,
content: String,
author_id: String,
created_at: i64,
}
#[derive(Debug, Serialize, Deserialize)]
struct NewPost {
title: String,
content: String,
}
List Records
use trailbase_client::{ListArguments, Pagination};
let posts = client.records::<Post>("posts");
let args = ListArguments::new()
.with_pagination(Pagination::new().with_limit(10))
.with_order(&["-created_at"])
.with_count(true);
let response = posts.list(args).await?;
println!("Records: {}", response.records.len());
println!("Total count: {:?}", response.total_count);
println!("Next cursor: {:?}", response.cursor);
Read a Record
// Simple read with string ID
let post: Post = posts.read("post-id").await?;
// Read with integer ID
let post: Post = posts.read(123i64).await?;
// With expanded relationships
use trailbase_client::ReadArguments;
let args = ReadArguments::new("post-id")
.with_expand(&["author"]);
let post: Post = posts.read(args).await?;
println!("Title: {}", post.title);
Create a Record
let new_post = NewPost {
title: "Hello World".to_string(),
content: "My first post from Rust".to_string(),
};
let post_id = posts.create(&new_post).await?;
println!("Created post with ID: {}", post_id);
Update a Record
use serde_json::json;
let update = json!({
"title": "Updated Title"
});
posts.update("post-id", &update).await?;
Delete a Record
posts.delete("post-id").await?;
Filtering
use trailbase_client::{Filter, CompareOp, ValueOrFilterGroup};
// Simple equality filter
let args = ListArguments::new()
.with_filter(Filter::new("author_id", "user-123"));
let response = posts.list(args).await?;
// With comparison operators
let args = ListArguments::new()
.with_filter(
Filter::new("created_at", &week_ago.to_string())
.with_op(CompareOp::GreaterThan)
);
// LIKE operator for text search
let args = ListArguments::new()
.with_filter(
Filter::new("title", "%search%")
.with_op(CompareOp::Like)
);
// AND composite filter
use trailbase_client::FilterGroup;
let filters = FilterGroup::And(vec![
ValueOrFilterGroup::Value(Filter::new("status", "published")),
ValueOrFilterGroup::Value(Filter::new("author_id", user_id)),
]);
let args = ListArguments::new()
.with_filters(Some(filters));
Available Comparison Operators
pub enum CompareOp {
Equal,
NotEqual,
GreaterThanEqual,
GreaterThan,
LessThanEqual,
LessThan,
Like,
Regexp,
StWithin, // Geospatial
StIntersects, // Geospatial
StContains, // Geospatial
}
Real-time Subscriptions
Subscribe to Record Changes
use futures_lite::StreamExt;
use trailbase_client::DbEvent;
// Subscribe to a single record
let mut stream = posts.subscribe("post-id").await?;
while let Some(event) = stream.next().await {
match event {
DbEvent::Insert(value) => {
println!("Record inserted: {:?}", value);
}
DbEvent::Update(value) => {
println!("Record updated: {:?}", value);
}
DbEvent::Delete(value) => {
println!("Record deleted: {:?}", value);
}
DbEvent::Error(msg) => {
eprintln!("Subscription error: {}", msg);
}
}
}
Subscribe to All Records
let filters = Some(Filter::new("author_id", user_id));
let mut stream = posts.subscribe_all(filters).await?;
while let Some(event) = stream.next().await {
println!("Change event: {:?}", event);
}
WebSocket Subscriptions
With thews feature enabled:
#[cfg(feature = "ws")]
{
let mut stream = posts.subscribe_ws("post-id", None).await?;
while let Some(event) = stream.next().await {
println!("WebSocket event: {:?}", event);
}
}
Error Handling
use trailbase_client::Error;
match posts.read("post-id").await {
Ok(post) => println!("Post: {:?}", post),
Err(Error::HttpStatus(status)) => {
eprintln!("HTTP error: {}", status);
}
Err(Error::MissingRefreshToken) => {
eprintln!("No refresh token available");
}
Err(e) => {
eprintln!("Error: {}", e);
}
}
Type Definitions
User
pub struct User {
pub sub: String,
pub email: String,
}
Tokens
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Tokens {
pub auth_token: String,
pub refresh_token: Option<String>,
pub csrf_token: Option<String>,
}
ListResponse
#[derive(Clone, Debug, Deserialize)]
pub struct ListResponse<T> {
pub cursor: Option<String>,
pub total_count: Option<usize>,
pub records: Vec<T>,
}
Pagination
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Pagination {
// Private fields
}
impl Pagination {
pub fn new() -> Self;
pub fn with_limit(self, limit: impl Into<Option<usize>>) -> Pagination;
pub fn with_cursor(self, cursor: impl Into<Option<String>>) -> Pagination;
pub fn with_offset(self, offset: impl Into<Option<usize>>) -> Pagination;
}
DbEvent
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum DbEvent {
Update(Option<serde_json::Value>),
Insert(Option<serde_json::Value>),
Delete(Option<serde_json::Value>),
Error(String),
}
RecordId Trait
TheRecordId trait allows using different types as record identifiers:
use trailbase_client::RecordId;
// String
let post: Post = posts.read("post-id").await?;
// &str
let post: Post = posts.read("post-id").await?;
// i64
let post: Post = posts.read(123i64).await?;
// String (owned)
let id = String::from("post-id");
let post: Post = posts.read(id).await?;
Best Practices
Use the
#[derive(Serialize, Deserialize)] macros on your record types for seamless serialization.Store tokens securely and never commit them to version control. Consider using environment variables or secure credential storage.
The client automatically refreshes auth tokens before they expire. Manual refresh is rarely needed.
Example Application
use trailbase_client::{Client, Filter, ListArguments, Pagination};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct Post {
id: String,
title: String,
content: String,
published: bool,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize client
let client = Client::new(
std::env::var("TRAILBASE_URL")
.unwrap_or_else(|_| "http://localhost:4000".to_string())
).await?;
// Login
client.login(
&std::env::var("TRAILBASE_EMAIL")?,
&std::env::var("TRAILBASE_PASSWORD")?,
).await?;
if let Some(user) = client.user() {
println!("Logged in as: {}", user.email);
}
// List posts
let posts = client.records::<Post>("posts");
let args = ListArguments::new()
.with_pagination(Pagination::new().with_limit(10))
.with_order(&["-created_at"])
.with_filter(Filter::new("published", "true"));
let response = posts.list(args).await?;
println!("\nFound {} posts:", response.records.len());
for post in &response.records {
println!("- {}", post.title);
}
// Create a new post
let new_post = serde_json::json!({
"title": "Hello from Rust",
"content": "This post was created using the TrailBase Rust SDK",
"published": true
});
let new_post_id = posts.create(&new_post).await?;
println!("\nCreated new post with ID: {}", new_post_id);
// Read the post
let post: Post = posts.read(&new_post_id).await?;
println!("Post title: {}", post.title);
// Logout
client.logout().await?;
println!("\nLogged out");
Ok(())
}