Code Monkey home page Code Monkey logo

postgrest-rs's Introduction

postgrest-rs

Build Crate API License: Apache-2.0 OR MIT

PostgREST client-side library 🦀. This library provides an ORM interface to PostgREST.

Usage

Add this to your Cargo.toml:

[dependencies]
postgrest = "1.0"

Simple example:

use postgrest::Postgrest;

let client = Postgrest::new("https://your.postgrest.endpoint");
let resp = client
    .from("your_table")
    .select("*")
    .execute()
    .await?;
let body = resp
    .text()
    .await?;

Simple example with JWT auth

use postgrest::Postgrest;

let client = Postgrest::new("https://your.postgrest.endpoint");
let resp = client
    .from("your_table")
    .auth("VerySensitiveJWTValueAsStringOrStr")
    .select("*")
    .execute()
    .await?;
let body = resp
    .text()
    .await?;

Simplified example using a custom header (e.g. for API gateway authentication with Supabase).

use postgrest::Postgrest;

let client = Postgrest::new("https://your.supabase.endpoint/rest/v1/")
    .insert_header("apikey", "ExampleAPIKeyValue"); // EXAMPLE ONLY!
// Don't actually hard code this value, that's really bad. Use environment
// variables like with the dotenv(https://crates.io/crates/dotenv) crate to inject
let resp = client
    .from("your_table")
    .select("*")
    .execute()
    .await?;
let body = resp
    .text()
    .await?;

Secure example with authenticated API gateway using the dotenv crate to correctly retrieve sensitive values.

use postgrest::Postgrest;
use dotenv;

dotenv::dotenv().ok(); 

let client = Postgrest::new("https://your.supabase.endpoint/rest/v1/")
    .insert_header(
        "apikey",
        dotenv::var("SUPABASE_PUBLIC_API_KEY").unwrap())
let resp = client
    .from("your_table")
    .select("*")
    .execute()
    .await?;
let body = resp
    .text()
    .await?;

Building Queries

These examples assume you've already initialized the client. The methods .from() and .rpc() initalizes the query builder inside the client.

Using filters:

let resp = client
    .from("your_table")
    .eq("country", "Germany")
    .gte("id", "20")
    .select("*")
    .execute()
    .await?;

Updating a table:

let resp = client
    .from("your_table")
    .eq("username", "soedirgo")
    .update("{\"organization\": \"supabase\"}")
    .execute()
    .await?;

Executing stored procedures:

let resp = client
    .rpc("add", "{\"a\": 1, \"b\": 2}")
    .execute()
    .await?;

Not enough filters:

let resp = client
    .from("countries")
    .eq("name", "New Zealand")                        // You can filter for equality...
    .gt("id", "20")
    .lt("id", "20")
    .gte("id", "20")
    .lte("id", "20")
    .like("name", "%United%")                         // ...do pattern matching...
    .ilike("name", "%United%")
    .is("name", "null")
    .in_("name", vec!["China", "France"])
    .neq("name", "China")
    .fts("phrase", "The Fat Cats", Some("english"))   // ...do full text search...
    .plfts("phrase", "The Fat Cats", None)
    .phfts("phrase", "The Fat Cats", Some("english"))
    .wfts("phrase", "The Fat Cats", None)
    .cs("countries", "(10,20)")
    .cd("countries", "(10,20)")
    .ov("population_range", "(100,500)")
    .sl("population_range", (100, 500))               // ...and range operations!
    .sr("population_range", (100, 500))               // Find out more about the filters at:
    .nxl("population_range", (100, 500))              // https://postgrest.org/en/stable/api.html#operators
    .nxr("population_range", (100, 500))
    .adj("population_range", (100, 500))
    .select("*")
    .execute()
    .await?;

Check out the API docs for more info!

Contributing

Contributions are welcome! There might be some features you want in, or some unclear documentation, or a bug—either way, feel free to create an issue, and we'll work it out!

Boring stuff below.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as below, without any additional terms or conditions.

License

Licensed under either of

at your option.


postgrest-rs's People

Contributors

gatewaynode avatar jgraettinger avatar jnhyatt avatar jshearer avatar liamkinne avatar mdibaiee avatar rodneylab avatar sei0o avatar siddharthborderwala avatar soedirgo avatar spencerbart avatar srid avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

postgrest-rs's Issues

Implement Future for Builder

Currently, users have to call execute at the end of a request:

let resp = client
    .from("table")
    .select("*")
    .execute()
    .await?;

To omit this, we need to implement the Future trait, which opens a can of worms that I really don't want to deal with right now. For now, the plan is to cross fingers and wait for this to be implemented directly into reqwest's RequestBuilder (see issue 908 in reqwest).

Support json as parameter

Hello!

Thanks for the amazing work :) Would it be possible to add Serde json support to perform queries based on json input?

Implement Typestate

Problem
Current Builder pattern is very error prone.

Proposal
Introduce Typestate.

`Postgrest` can be `Clone`

Feature request

Hi!

Is your feature request related to a problem? Please describe.

I would like to be able to pass my Postgrest around my program to be able to make requests from multiple places, notably from async tasks I spawn periodically. My approach would be:

let client = postgrest.clone();
let task = task_pool.spawn(async move {
    let result = build_query(client).execute().await;
    handle_result(result).await
});

Describe alternatives you've considered

Leave as is and require the user to either:

  • Create a new Postgrest at each call site. The downside to this would be that all information needed to build the client must be available as well. This breaks encapsulation as it requires users to "ship" state (access tokens, etc.) around instead of the Postgrest object, which could just do this for you.
  • Use Rc or similar to share the Postgrest object throughout the program. This feels redundant as Client already stores an Arc to all the persistent, non-Clone state.

Additional context

Two facts jump out at me when looking at the implementation:

  • Client is Clone (as are all other fields)
  • Constructing a Postgrest is a simple matter -- very little state is stored in the structure
    Both of these hint to me that cloning should probably be a feature of the public API: if Postgrest only represents the state necessary to make a request, that state should probably be cloneable as it behaves similarly to POD.

Let me know what you think!

Is this package working?

What happening here? I try to write a little program to update 2 tables. I got 200 for deletion but nothing was deleted. The second try is an insertion. The code generates a valid JSON, it was checked, but I always got a 401 error. I followed the docs but nothing works. Everything is implemented in typescript too in the same way but the difference is only that it works.

syncronization started...
connecting to supabase...
connected to supabase...
loading data...
data loaded...
processing data...
data processed...
clearing database...
res: Response { url: Url { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("hxtfhsirbifxdggocayk.supabase.co")), port: None, path: "/rest/v1//eans", query: Some("id=neq.b8c3ccbd-1a10-4724-82e1-42813a06e339"), fragment: None }, status: 200, headers: {"date": "Mon, 13 Feb 2023 23:46:47 GMT", "content-type": "application/json; charset=utf-8", "cf-ray": "799172044cd83249-VIE", "access-control-allow-origin": "*", "content-range": "*/*", "strict-transport-security": "max-age=2592000; includeSubDomains", "vary": "Accept-Encoding", "via": "kong/2.8.1", "cf-cache-status": "DYNAMIC", "content-profile": "public", "sb-gateway-version": "1", "x-kong-proxy-latency": "2", "x-kong-upstream-latency": "9", "server": "cloudflare", "alt-svc": "h3=\":443\"; ma=86400, h3-29=\":443\"; ma=86400"} }
res: Response { url: Url { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("hxtfhsirbifxdggocayk.supabase.co")), port: None, path: "/rest/v1//products", query: Some("id=neq.b8c3ccbd-1a10-4724-82e1-42813a06e339"), fragment: None }, status: 200, headers: {"date": "Mon, 13 Feb 2023 23:46:47 GMT", "content-type": "application/json; charset=utf-8", "cf-ray": "799172053d803249-VIE", "access-control-allow-origin": "*", "content-range": "*/*", "strict-transport-security": "max-age=2592000; includeSubDomains", "vary": "Accept-Encoding", "via": "kong/2.8.1", "cf-cache-status": "DYNAMIC", "content-profile": "public", "sb-gateway-version": "1", "x-kong-proxy-latency": "0", "x-kong-upstream-latency": "11", "server": "cloudflare", "alt-svc": "h3=\":443\"; ma=86400, h3-29=\":443\"; ma=86400"} }
uploading data...
res: Response { url: Url { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("hxtfhsirbifxdggocayk.supabase.co")), port: None, path: "/rest/v1//products", query: None, fragment: None }, status: 401, headers: {"date": "Mon, 13 Feb 2023 23:46:49 GMT", "content-type": "application/json; charset=utf-8", "cf-ray": "79917205ee033249-VIE", "access-control-allow-origin": "*", "strict-transport-security": "max-age=2592000; includeSubDomains", "via": "kong/2.8.1", "www-authenticate": "Bearer", "cf-cache-status": "DYNAMIC", "sb-gateway-version": "1", "x-kong-proxy-latency": "110", "x-kong-upstream-latency": "580", "server": "cloudflare", "alt-svc": "h3=\":443\"; ma=86400, h3-29=\":443\"; ma=86400"} }
res: Response { url: Url { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("hxtfhsirbifxdggocayk.supabase.co")), port: None, path: "/rest/v1//eans", query: None, fragment: None }, status: 401, headers: {"date": "Mon, 13 Feb 2023 23:46:50 GMT", "content-type": "application/json; charset=utf-8", "cf-ray": "799172130a3f3249-VIE", "access-control-allow-origin": "*", "strict-transport-security": "max-age=2592000; includeSubDomains", "via": "kong/2.8.1", "www-authenticate": "Bearer", "cf-cache-status": "DYNAMIC", "sb-gateway-version": "1", "x-kong-proxy-latency": "103", "x-kong-upstream-latency": "249", "server": "cloudflare", "alt-svc": "h3=\":443\"; ma=86400, h3-29=\":443\"; ma=86400"} }
data uploaded...
syncronization finished...
use dotenvy::{dotenv, var};
use postgrest::Postgrest;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
struct Product {
    title: String,
    net_price: String,
    total_price: String,
    product_id: String,
}

#[derive(Serialize, Deserialize, Debug)]
struct Ean {
    ean: String,
    product_id: String,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    dotenv().ok().unwrap();

    println!("syncronization started...");
    println!("connecting to supabase...");
    let client = Postgrest::new(var("SUPABASE_URL").unwrap())
        .insert_header("apikey", var("SUPABASE_KEY").unwrap());
    println!("connected to supabase...");
    println!("loading data...");
    let input = std::fs::read_to_string("data.csv").expect("file not found");
    println!("data loaded...");
    println!("processing data...");
    let data = input.split("\n").collect::<Vec<&str>>();
    let mut formatted_data = vec![];

    for (index, line) in data.iter().enumerate() {
        if index == 0 || index == 1 || index == data.len() - 1 || index == data.len() - 2 {
            continue;
        }

        let splitted_line = line.split(';').collect::<Vec<&str>>();
        formatted_data.push(splitted_line);
    }

    let filtered_data = formatted_data
        .iter()
        .filter(|line| !line.contains(&""))
        .collect::<Vec<&Vec<&str>>>();

    let mut products = vec![];
    let mut eans = vec![];

    for line in filtered_data {
        let product = Product {
            title: line[2].to_string(),
            net_price: line[3].to_string(),
            total_price: line[4].replace("\r", ""),
            product_id: line[0].to_string(),
        };
        let ean = Ean {
            ean: line[1].to_string(),
            product_id: line[0].to_string(),
        };

        products.push(product);
        eans.push(ean);
    }
    // generate json from rust struct
    let products_json = serde_json::to_string(&products).unwrap();
    let eans_json = serde_json::to_string(&eans).unwrap();
    println!("data processed...");
    println!("clearing database...");
    let delete_eans = client
        .from("eans")
        .neq("id", "b8c3ccbd-1a10-4724-82e1-42813a06e339")
        .delete()
        .execute()
        .await;

    match delete_eans {
        Ok(res) => println!("res: {:?}", res),
        Err(err) => println!("error: {:?}", err),
    }

    let delete_products = client
        .from("products")
        .neq("id", "b8c3ccbd-1a10-4724-82e1-42813a06e339")
        .delete()
        .execute()
        .await;

    match delete_products {
        Ok(res) => println!("res: {:?}", res),
        Err(err) => println!("error: {:?}", err),
    }

    println!("uploading data...");
    let upload_products = client
        .from("products")
        .insert(products_json)
        .execute()
        .await;

    match upload_products {
        Ok(res) => println!("res: {:?}", res),
        Err(err) => println!("error: {:?}", err),
    }

    let upload_eans = client.from("eans").insert(eans_json).execute().await;

    match upload_eans {
        Ok(res) => println!("res: {:?}", res),
        Err(err) => println!("error: {:?}", err),
    };

    println!("data uploaded...");
    println!("syncronization finished...");

    Ok(())
}

Wasm support

This library doesn't support wasm. There are two dependencies,

I assume that once tokio is ported to support Wasm, then postgrest-rs should automatically support wasm with no code change.

Save and re-use reqwest instance

Currently the reqwest client seem to be created always for executing a query:

let mut req = Client::new().request(self.method.clone(), &self.url);

This will end up doing a SSL handshake every time. It's better to create it once and reuse it to improve performance.

clean_param on `eq` leads to filter not working as expected

Hello!

We are using this client in our project here, and we have noticed that due to automatically quoting of the values if there is a reserved character, it seems to lead to queries not finding results.

In our case, we are using this snippet:

cfg.client()?
                .from("live_specs")
                .eq("catalog_name", name)
                .select("catalog_name,last_pub_id,pub_id:last_pub_id,spec,spec_type")
                .single(),

This leads to this query being built:

RequestBuilder {
    method: GET,
    url: Url {
        scheme: "https",
        cannot_be_a_base: false,
        username: "",
        password: None,
        host: Some(
            Domain(
                "<secret>.supabase.co",
            ),
        ),
        port: None,
        path: "/rest/v1/live_specs",
        query: Some(
            "catalog_name=eq.%22trial%2Fmahdi%2Ftest%2Ftest.json.zst%22&select=catalog_name%2Clast_pub_id%2Cpub_id%3Alast_pub_id%2Cspec%2Cspec_type",
        ),
        fragment: None,
    },
    headers: {
        "apikey": "<SECRET>",
        "authorization": "Bearer <SECRET>",
        "accept": "application/vnd.pgrst.object+json",
    },
}

This query lends no results, however when we manually try this query, without the encoded quotes (%22), it does lend the expected results, so this query lends no results:

curl 'https://eyrcnmuzzyriypdajwdk.supabase.co/rest/v1/live_specs_ext?select=*&catalog_name=eq.%22trial%2Fmahdi%2Ftest%2Ftest.json.zst%22&select=catalog_name%2Clast_pub_id%2Cpub_id%3Alast_pub_id%2Cspec%2Cspec_type&order=updated_at.desc.nullslast&offset=0&limit=10&spec_type=eq.collection'

But this does:

curl 'https://eyrcnmuzzyriypdajwdk.supabase.co/rest/v1/live_specs_ext?select=*&catalog_name=eq.trial%2Fmahdi%2Ftest%2Ftest.json.zst&select=catalog_name%2Clast_pub_id%2Cpub_id%3Alast_pub_id%2Cspec%2Cspec_type&order=updated_at.desc.nullslast&offset=0&limit=10&spec_type=eq.collection'

I looked around and I found this issue on PostgREST where someone is reporting a similar behaviour:

is it about the filter syntax containing reserved characters, or the filter value? (e.g. I was expecting a consisten way of quoting, but it turns out that =eq. doesn't need any escaping)

So apparently eq does not need quoting of the value.

sort filter

need a sort filter

i cannot find a way to include sort in sql query is there a way to add this?

Add reqwest's blocking Client feature

Feature request

Is your feature request related to a problem? Please describe.

Currently Postgrest is only usable inside tokio's runtime. That's sufficient for most applications, but there are some environments that either have a different future crate, or just can't afford to use tokio (heavy binary footprint).

In my case, I'm using bevy that support only futures_lite futures. There is a way to use tokio, but it's painful and it just loses the point of using tokio in the first place (you have to manually poll it. An overkill just to use postgrest).

Describe the solution you'd like

My solution would be to create a crate feature blocking (like in reqwest), which contains the blocking module, where Postgrest uses the blocking reqwest client instead. The logic should be absolutely the same:

use postgrest::blocking::Postgrest;

let pg = Postgrest::new(url);

Describe alternatives you've considered

Currently none. Just use the postgrest syntax directly with blocking reqwest or ureq.

Additional context

The feature can also be very useful not only in game development, but for simple apps in general that can afford to block the current thread.

Expose the reqwest::Error

Feature request

Export the reqwest::Error

Is your feature request related to a problem? Please describe.

I was trying to map the Error and realized that the error in the Result that is returned by execute is the reqwest::Error but I don't have access to it unless I add the reqwest crate to my project. This may lead to incompatibility in certain cases and should be avoided.

Describe the solution you'd like

add pub use reqwest::Error to lib.rs

Describe alternatives you've considered

Manually adding the reqwest crate

.eq doesn't work with querying foreign tables

Bug report

Describe the bug

.eq querying a foreign table doesn't work

To Reproduce

Try to query a foreign table with .eq

Expected behavior

This is the equivalent JavaScript query and it works just fine.

Screenshot 2022-11-18 at 1 53 29 PM

Additional context

I have a table called schedule and a table called schedule_details that stores information on each individual day (so there are 7 in total). I am trying to reach the table schedule_details and do .eq(schedule_details.purchase_day, 5) where I only get the information about today.

Screenshot 2022-11-18 at 1 47 59 PM

This is the error that comes back

Screenshot 2022-11-18 at 1 52 10 PM

Add the Authorization header in the README.md examples regarding headers

Improve documentation

Link

README.md

Describe the problem

Supabase seems to need the Authorization token as well as the apikey token. It took me some time to realize it.
Without that, even the service_role api key doesn't work unfortunately. It could be valuable for other users so they can copy the snippets directly.

I put it as an issue instead of a PR, because I am not sure if it's always the case.

Some discussion regarding the Authorization header: https://github.com/orgs/supabase/discussions/3963

Describe the improvement

Update the snippet to add the insertion of Authorization header in the sections:

  • Simplified example using a custom header (e.g. for API gateway authentication with Supabase).
  • Secure example with authenticated API gateway using the dotenv crate to correctly retrieve sensitive values.

Note: the Authorization header value needs to be on the format Bearer <token-value>:

  • Example:
    .insert_header("Authorization", format!("Bearer {}", token));

Filters such as ".gt", ".gte", ".lt", and "lte" don't work with numbers

Bug report

Describe the bug

Filters such as ".gt", ".gte", ".lt", and "lte" don't work with numbers. The builder constructs the following string when using the .gt filter

http://localhost:54321/rest/v1//business_locations?select=*&latitude=gte.%2240.242423%22

when it should be creating the URL

http://localhost:54321/rest/v1//business_locations?select=*&latitude=gte.40.242423

To Reproduce

Try using the .gt filter on a column with a numeric type

Expected behavior

It should construct a URL without URL encoded quotes

Screenshots

2022-10-02T08:09:21Z DEBUG reqwest::async_impl::client] response '400 Bad Request' for http://localhost:54321/rest/v1//business_locations?select=*&latitude=gte.%2240.242423%22

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.