diff --git a/Cargo.toml b/Cargo.toml index 69f5834..e498fe3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,3 +7,6 @@ edition = "2018" [dependencies] reqwest = { version = "0.10", features = ["json"] } tokio = { version = "0.2", features = ["full"] } + +[dev-dependencies] +json = "0.12" diff --git a/src/builder.rs b/src/builder.rs index 4490025..dd3deb9 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1,5 +1,3 @@ -extern crate reqwest; - use reqwest::{ header::{HeaderMap, HeaderValue}, Client, Error, Method, Response, @@ -19,8 +17,9 @@ pub struct Builder { // TODO: Complex filters (not, and, or) // TODO: Exact, planned, estimated count (HEAD verb) // TODO: Response format -// TODO: Embedded resources -// TODO: Content type (csv, etc.) +// TODO: Resource embedding (embedded filters, etc.) +// TODO: Content-Type (text/csv, etc.) +// TODO: Reject update/delete w/o filters impl Builder { pub fn new(url: S, schema: Option) -> Self where @@ -126,10 +125,9 @@ impl Builder { S: Into, { self.method = Method::POST; - self.headers.append( + self.headers.insert( "Prefer", - // Maybe check if this works as intended... - HeaderValue::from_static("return=representation; resolution=merge-duplicates"), + HeaderValue::from_static("return=representation,resolution=merge-duplicates"), ); self.body = Some(body.into()); self @@ -143,7 +141,7 @@ impl Builder { { self.method = Method::PUT; self.headers - .append("Prefer", HeaderValue::from_static("return=representation")); + .insert("Prefer", HeaderValue::from_static("return=representation")); self.queries .push((primary_column.into(), format!("eq.{}", key.into()))); self.body = Some(body.into()); @@ -156,7 +154,7 @@ impl Builder { { self.method = Method::PATCH; self.headers - .append("Prefer", HeaderValue::from_static("return=representation")); + .insert("Prefer", HeaderValue::from_static("return=representation")); self.body = Some(body.into()); self } @@ -164,7 +162,7 @@ impl Builder { pub fn delete(mut self) -> Self { self.method = Method::DELETE; self.headers - .append("Prefer", HeaderValue::from_static("return=representation")); + .insert("Prefer", HeaderValue::from_static("return=representation")); self } @@ -189,6 +187,10 @@ impl Builder { self.headers .append(key, HeaderValue::from_str(&schema).unwrap()); } + if self.method != Method::GET && self.method != Method::HEAD { + self.headers + .insert("Content-Type", HeaderValue::from_static("application/json")); + } req = req.headers(self.headers).query(&self.queries); if let Some(body) = self.body { req = req.body(body); @@ -278,7 +280,7 @@ mod tests { let builder = Builder::new(TABLE_URL, None).upsert("ignored"); assert_eq!( builder.headers.get("Prefer").unwrap(), - HeaderValue::from_static("return=representation; resolution=merge-duplicates") + HeaderValue::from_static("return=representation,resolution=merge-duplicates") ); } @@ -303,4 +305,21 @@ mod tests { assert_eq!(builder.body.unwrap(), "{\"a\": 1, \"b\": 2}"); assert_eq!(builder.is_rpc, true); } + + #[test] + fn chain_filters() -> Result<(), Box> { + let builder = Builder::new(TABLE_URL, None) + .eq("username", "supabot") + .neq("message", "hello world") + .gte("channel_id", "1") + .select("*"); + + let queries = builder.queries; + assert_eq!(queries.len(), 4); + assert!(queries.contains(&("username".into(), "eq.supabot".into()))); + assert!(queries.contains(&("message".into(), "neq.hello world".into()))); + assert!(queries.contains(&("channel_id".into(), "gte.1".into()))); + + Ok(()) + } } diff --git a/src/lib.rs b/src/lib.rs index 9ff85f1..41fecc4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +extern crate reqwest; + mod builder; mod filter; diff --git a/tests/client.rs b/tests/client.rs new file mode 100644 index 0000000..43af701 --- /dev/null +++ b/tests/client.rs @@ -0,0 +1,159 @@ +use postgrest::Postgrest; + +use std::error::Error; + +const REST_URL: &str = "http://localhost:3000"; + +#[tokio::test] +async fn basic_data() -> Result<(), Box> { + let client = Postgrest::new(REST_URL); + let resp = client + .from("users") + .select("username") + .eq("status", "OFFLINE") + .execute() + .await?; + let body = resp.text().await?; + let body = json::parse(&body)?; + + assert_eq!(body[0]["username"], "kiwicopple"); + + Ok(()) +} + +#[tokio::test] +async fn relational_join() -> Result<(), Box> { + let client = Postgrest::new(REST_URL); + let resp = client + .from("channels") + .select("slug, messages(message)") + .eq("slug", "public") + .execute() + .await?; + let body = resp.text().await?; + let body = json::parse(&body)?; + + assert_eq!(body[0]["messages"][0]["message"], "Hello World 👋"); + assert_eq!(body[0]["slug"], "public"); + + Ok(()) +} + +#[tokio::test] +async fn insert() -> Result<(), Box> { + let client = Postgrest::new(REST_URL); + let resp = client + .from("messages") + .insert(r#"[{"message": "Test message 0", "channel_id": 1, "username": "kiwicopple"}]"#) + .execute() + .await?; + let status = resp.status(); + + assert_eq!(status.as_u16(), 201); + + Ok(()) +} + +#[tokio::test] +async fn upsert() -> Result<(), Box> { + let client = Postgrest::new(REST_URL); + let resp = client + .from("users") + .upsert( + r#"[{"username": "dragarcia", "status": "OFFLINE"}, + {"username": "supabot2", "status": "ONLINE"}]"#, + ) + .execute() + .await?; + let body = resp.text().await?; + let body = json::parse(&body)?; + + assert_eq!(body[0]["username"], "dragarcia"); + assert_eq!(body[1]["username"], "supabot2"); + + Ok(()) +} + +#[tokio::test] +async fn upsert_existing() -> Result<(), Box> { + let client = Postgrest::new(REST_URL); + let resp = client + .from("users") + .upsert(r#"{"username": "dragarcia", "status": "ONLINE"}"#) + .execute() + .await?; + let body = resp.text().await?; + let body = json::parse(&body)?; + + assert_eq!(body[0]["username"], "dragarcia"); + assert_eq!(body[0]["status"], "ONLINE"); + + Ok(()) +} + +#[tokio::test] +async fn upsert_nonexisting() -> Result<(), Box> { + let client = Postgrest::new(REST_URL); + let resp = client + .from("users") + .upsert(r#"{"username": "supabot3", "status": "ONLINE"}"#) + .execute() + .await?; + let body = resp.text().await?; + let body = json::parse(&body)?; + + assert_eq!(body[0]["username"], "supabot3"); + assert_eq!(body[0]["status"], "ONLINE"); + + Ok(()) +} + +#[tokio::test] +async fn update() -> Result<(), Box> { + let client = Postgrest::new(REST_URL); + let resp = client + .from("users") + .eq("status", "ONLINE") + .update(r#"{"status": "ONLINE"}"#) + .execute() + .await?; + let status = resp.status(); + let body = resp.text().await?; + let body = json::parse(&body)?; + + assert_eq!(status.as_u16(), 200); + assert_eq!(body[0]["status"], "ONLINE"); + + Ok(()) +} + +#[tokio::test] +async fn delete() -> Result<(), Box> { + let client = Postgrest::new(REST_URL); + let resp = client + .from("messages") + .neq("username", "supabot") + .delete() + .execute() + .await?; + let status = resp.status(); + + assert_eq!(status.as_u16(), 200); + + Ok(()) +} + +#[tokio::test] +async fn rpc() -> Result<(), Box> { + let client = Postgrest::new(REST_URL); + let resp = client + .rpc("get_status", r#"{"name_param": "leroyjenkins"}"#) + .execute() + .await?; + let body = resp.text().await?; + let body = json::parse(&body)?; + + assert!(body.is_null()); + + Ok(()) +} diff --git a/tests/db/docker-compose.yml b/tests/db/docker-compose.yml index ea0d426..8eb0a38 100644 --- a/tests/db/docker-compose.yml +++ b/tests/db/docker-compose.yml @@ -1,7 +1,7 @@ version: "3" services: rest: - image: postgrest/postgrest + image: postgrest/postgrest:v7.0.1 ports: - "3000:3000" environment: diff --git a/tests/multi_schema.rs b/tests/multi_schema.rs new file mode 100644 index 0000000..70f80fd --- /dev/null +++ b/tests/multi_schema.rs @@ -0,0 +1,152 @@ +#[macro_use] +extern crate json; // array!, object!, value! + +use postgrest::Postgrest; + +use std::error::Error; + +const REST_URL: &str = "http://localhost:3000"; + +#[tokio::test] +async fn read_other_schema() -> Result<(), Box> { + let client = Postgrest::new(REST_URL); + let resp = client + .from("users") + .select("username") + .eq("username", "leroyjenkins") + .execute() + .await?; + let body = resp.text().await?; + let body = json::parse(&body)?; + + assert_eq!(body, array![]); + + let other_client = Postgrest::new(REST_URL).schema("personal"); + let other_resp = other_client + .from("users") + .select("username") + .eq("username", "leroyjenkins") + .execute() + .await?; + let other_body = other_resp.text().await?; + let other_body = json::parse(&other_body)?; + + assert_eq!(other_body, array![{"username": "leroyjenkins"}]); + + Ok(()) +} + +#[tokio::test] +async fn write_other_schema() -> Result<(), Box> { + let client = Postgrest::new(REST_URL); + let resp = client + .from("users") + .select("status") + .eq("username", "supabot") + .execute() + .await?; + let body = resp.text().await?; + let body = json::parse(&body)?; + + assert_eq!(body[0]["status"], "ONLINE"); + + let other_client = Postgrest::new(REST_URL).schema("personal"); + let other_resp = other_client + .from("users") + .update("{\"status\": \"OFFLINE\"}") + .eq("username", "supabot") + .execute() + .await?; + let other_body = other_resp.text().await?; + let other_body = json::parse(&other_body)?; + + assert_eq!(other_body[0]["status"], "OFFLINE"); + + Ok(()) +} + +#[tokio::test] +async fn read_nonexisting_schema() -> Result<(), Box> { + let client = Postgrest::new(REST_URL).schema("private"); + let resp = client.from("channels").select("*").execute().await?; + let body = resp.text().await?; + let body = json::parse(&body)?; + + assert_eq!( + body["message"], + "The schema must be one of the following: public, personal" + ); + + Ok(()) +} + +#[tokio::test] +async fn write_nonexisting_schema() -> Result<(), Box> { + let client = Postgrest::new(REST_URL).schema("private"); + let resp = client + .from("channels") + .update("{\"slug\": \"private\"}") + .eq("slug", "random") + .execute() + .await?; + let body = resp.text().await?; + let body = json::parse(&body)?; + + assert_eq!( + body["message"], + "The schema must be one of the following: public, personal" + ); + + Ok(()) +} + +#[tokio::test] +async fn other_schema_rpc() -> Result<(), Box> { + let client = Postgrest::new(REST_URL).schema("personal"); + let resp = client + .rpc("get_status", "{\"name_param\": \"leroyjenkins\"}") + .execute() + .await?; + let body = resp.text().await?; + let body = json::parse(&body)?; + + assert_eq!(body, "ONLINE"); + + Ok(()) +} + +#[tokio::test] +async fn nonexisting_rpc_in_schema() -> Result<(), Box> { + let client = Postgrest::new(REST_URL).schema("personal"); + let resp = client + .rpc("nonexistent_procedure", "{\"param\": 0}") + .execute() + .await?; + let body = resp.text().await?; + let body = json::parse(&body)?; + + assert_eq!( + body["message"], + "function personal.nonexistent_procedure(param => text) does not exist" + ); + + Ok(()) +} + +#[tokio::test] +async fn nonexisting_schema_for_rpc() -> Result<(), Box> { + let client = Postgrest::new(REST_URL).schema("private"); + let resp = client + .rpc("get_status", "{\"name_param\": \"leroyjenkins\"}") + .execute() + .await?; + let body = resp.text().await?; + let body = json::parse(&body)?; + + assert_eq!( + body["message"], + "The schema must be one of the following: public, personal" + ); + + Ok(()) +}