#supabase #postgresql #postgrest #api-bindings

bin+lib supabase-rust-postgrest

PostgreSQL REST client for Supabase

6 releases

new 0.4.0 Apr 26, 2025
0.2.0 Apr 25, 2025
0.1.4 Apr 23, 2025

#2093 in Database interfaces

Download history 500/week @ 2025-04-20

500 downloads per month
Used in 2 crates

MIT license

94KB
2K SLoC

Supabase PostgREST Client for Rust

A Rust client library for Supabase PostgreSQL REST API access.

Features

  • Basic CRUD operations (select, insert, update, delete)
  • Filtering (eq, gt, lt, like, ilike, in_list, not, contains, contained_by, text_search, etc.)
  • Ordering (order) and pagination (limit, offset)
  • Joins (inner_join, left_join, include, referenced_by)
  • Transactions (begin_transaction, commit, rollback, savepoint)
  • RPC function calls (rpc)
  • CSV export (export_csv)
  • TypeScript to Rust type conversion infrastructure (via schema-convert feature, conversion logic is currently a placeholder)
  • Basic type-safe operation helpers (requires schema-convert feature, experimental)

Project Status & Roadmap

Current Status: Alpha (v0.1.3) - Core API Implemented, Type Safety Experimental

This crate provides core PostgREST functionality, transaction support, and initial infrastructure for type generation. It is under active development.

Roadmap:

  • Basic CRUD, Filtering, Ordering, Pagination
  • RPC Function Calls
  • Transactions & Savepoints
  • CSV Export
  • Full-text search (text_search)
  • Basic JSONB operations (contains, contained_by)
  • Support for more complex select queries (e.g., advanced nested resources/embedding)
  • Complete the schema-convert feature for robust TypeScript -> Rust type generation.
  • Enhance type-safe operation helpers based on generated types.
  • Improved error reporting and handling details.
  • Comprehensive integration tests against a live Supabase instance.
  • Explore potential state machine usage for request building/transaction management.

Installation

Add the dependency to your Cargo.toml:

[dependencies]
supabase-rust-postgrest = "0.1.3"

To use the TypeScript to Rust type conversion feature:

[dependencies]
supabase-rust-postgrest = { version = "0.1.3", features = ["schema-convert"] }

Basic Usage

use supabase_rust_postgrest::PostgrestClient;
use reqwest::Client;

async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let http_client = Client::new();
    let db = PostgrestClient::new(
        "https://your-project.supabase.co", 
        "your-anon-key", 
        "your_table", 
        http_client
    );
    
    // Fetch data
    let response: Vec<serde_json::Value> = db
        .select("*")
        .eq("column", "value")
        .execute()
        .await?;
    
    // Insert data
    let data = serde_json::json!({
        "name": "John Doe",
        "email": "john@example.com"
    });
    
    let inserted = db
        .insert(&data)
        .await?;
        
    // Update data
    let update_data = serde_json::json!({
        "name": "Jane Doe"
    });
    
    let updated = db
        .eq("id", "1")
        .update(&update_data)
        .await?;
        
    // Delete data
    let deleted = db
        .eq("id", "1")
        .delete()
        .await?;
        
    Ok(())
}

TypeScript to Rust Type Conversion

This crate provides functionality to convert TypeScript type definitions generated by Supabase's supabase gen types typescript command into Rust types. To use this feature, you must enable the schema-convert feature.

Note: While the infrastructure and CLI tools for schema conversion are present, the core logic (convert_typescript_to_rust function) that performs the detailed type mapping from TypeScript interfaces/types to Rust structs/enums is currently a placeholder and requires further implementation based on specific project needs or more sophisticated parsing. The current implementation primarily sets up the file structure and basic generation.

The simplest way to generate Rust types from your Supabase schema is using the provided Makefile:

# Copy the Makefile from this repository to your project root
# Then run:
make gen-types-rust

This will:

  1. Generate TypeScript types using supabase gen types typescript
  2. Convert them to Rust types
  3. Place the generated Rust file in src/generated/schema.rs

You can customize the output location and module name:

make gen-types-rust TYPES_OUTPUT_DIR=src/models MODULE_NAME=database

For more options, run:

make help

Converting from Command Line

# Run in your repository root directory
supabase gen types typescript > types.ts

# Generate Rust types from TypeScript definitions
cargo run --features schema-convert --bin supabase-gen-rust -- \
    --input-file ./types.ts \
    --output-dir ./src/generated \
    --module-name schema

Converting Programmatically

// NOTE: Requires the 'schema-convert' feature to be enabled.
// The core conversion logic is currently a placeholder.
# #[cfg(feature = "schema-convert")]
# {
use std::path::Path;
use supabase_rust_postgrest::{
    convert_typescript_to_rust,
    SchemaConvertOptions,
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let input_file = Path::new("./types.ts"); // Ensure this file exists
    let options = SchemaConvertOptions::default();

    // This function call might only perform basic setup in the current version.
    let output_path = convert_typescript_to_rust(input_file, options)?;
    println!("Generated Rust types structure at: {:?}", output_path);

    Ok(())
}
# }

Type-Safe Database Operations

Note: The type-safe operations shown below rely on the schema-convert feature and the associated generated types. This feature is currently experimental, and the underlying schema conversion logic is incomplete. Use with caution and expect potential limitations or required manual adjustments.

// NOTE: Requires the 'schema-convert' feature and generated types.
# #[cfg(feature = "schema-convert")]
# {
use serde::{Deserialize, Serialize};
use supabase_rust_postgrest::{
    PostgrestClient, Table, PostgrestClientTypeExtension
};
use reqwest::Client; // Added missing import

// Assuming types are generated in src/generated/schema.rs
// mod generated { include!(\"../src/generated/schema.rs\"); }
// use generated::schema::*;

// Or define manually for demonstration:
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] // Added PartialEq for test assertion
struct User {
    // Make fields public if accessed directly outside the module
    pub id: Option<i32>,
    pub name: String,
    pub email: String,
}

impl Table for User {
    fn table_name() -> &\'static str {
        "users" // Ensure this matches your actual table name
    }
}

async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let http_client = reqwest::Client::new(); // Use the imported Client
    // Ensure base_url and api_key are correct
    let client = PostgrestClient::new(
        "http://localhost:54321", // Example: Replace with your Supabase URL
        "your-anon-key",      // Example: Replace with your Supabase Anon Key
        "", // Table name set via Table trait
        http_client,
    );

    // Example: Type-safe query (assuming User with id=1 exists)
    // let users: Vec<User> = client
    //     .query_typed::<User>()? // Use query_typed
    //     .eq("name", "John") // Filter example
    //     .execute()
    //     .await?;

    // Example: Type-safe insert
    let new_user = User {
        id: None, // ID is usually generated by the database
        name: "Alice".to_string(),
        email: "alice@example.com".to_string(),
    };

    // The insert_typed method seems missing or part of the experimental feature.
    // Using standard insert for now:
    // let inserted_raw = client
    //      .from(User::table_name()) // Use from() with table name
    //      .insert(&new_user)      // Standard insert takes serializable data
    //      .execute::<Vec<User>>() // Expecting a Vec<User> back if RETURNING data
    //      .await?;
    // let inserted: User = inserted_raw.into_iter().next().ok_or("Insert failed")?;


    // Example: Type-safe update (assuming a user exists)
    // let mut user_to_update = users.get(0).cloned().ok_or("No user to update")?;
    // user_to_update.name = "Bob".to_string();

    // Update requires filter + data. update_typed seems missing/experimental.
    // Using standard update:
    // let updated_raw = client
    //     .from(User::table_name())
    //     .eq("id", &user_to_update.id.unwrap().to_string())
    //     .update(&serde_json::json!({ "name": user_to_update.name })) // Pass update data
    //     .execute::<Vec<User>>() // Expecting updated user back
    //     .await?;
    // let updated: User = updated_raw.into_iter().next().ok_or("Update failed")?;


    // Example: Type-safe delete (assuming a user exists)
    // let user_to_delete = users.get(0).ok_or("No user to delete")?;
    // delete_typed seems missing/experimental. Using standard delete:
    // client
    //     .from(User::table_name())
    //     .eq("id", &user_to_delete.id.unwrap().to_string())
    //     .delete()
    //     .execute::<serde_json::Value>() // Delete often returns minimal info
    //     .await?;


    println!("Example operations completed (actual execution depends on setup and uncommenting)");

    Ok(())
}
# }

Testing

To run the tests for this crate, including feature-specific tests:

cargo test --all-features

We aim for high test coverage, particularly for core CRUD and filtering operations. Integration tests using wiremock simulate responses from the PostgREST API.

Security Considerations

  • API Keys: Ensure your Supabase URL and anon key (or service_role key if used) are stored and handled securely. Avoid hardcoding them directly in your source code. Consider using environment variables or a secrets management solution.
  • Input Validation: While this library promotes type safety, always validate user-provided input before constructing database queries, especially for filter values, to prevent potential injection issues or unintended data access.

Contributing

Contributions are welcome! Please feel free to open an issue or submit a pull request. For major changes, please open an issue first to discuss what you would like to change.

Ensure that your contributions pass all tests (cargo test --all-features) and adhere to the project's coding style (run cargo fmt).

License

MIT

Dependencies

~6–18MB
~253K SLoC