Skip to main content

neon_wasi_http/
lib.rs

1mod deserializer;
2mod neon_response;
3mod query_builder;
4mod request;
5mod transaction_builder;
6
7#[cfg(feature = "orm_beta")]
8pub mod orm;
9
10use anyhow::{Context, Result};
11pub use neon_response::{QueryResponse, QueryResult, TransactionResponse, TransactionResult};
12pub use query_builder::{Query, QueryBuilder};
13use request::post;
14
15pub use transaction_builder::{Transaction, TransactionBuilder};
16
17#[cfg(feature = "orm_beta")]
18pub use sql_macro::*;
19
20pub struct Client {
21    pub(crate) connection_string: String,
22    pub(crate) url: String,
23    #[cfg(target_os = "wasi")]
24    pub client: wstd::http::Client,
25    #[cfg(not(target_os = "wasi"))]
26    pub client: reqwest::Client,
27}
28
29impl Client {
30    /// Returns and error if NEON_CONNECTION_STRING isn't set.
31    pub fn new_from_env() -> Result<Self> {
32        let connection_string = std::env::var("NEON_CONNECTION_STRING")
33            .context("ENV \"NEON_CONNECTION_STRING\" isn't set")?;
34
35        Self::new(&connection_string)
36    }
37
38    pub fn new(connection_string: &str) -> Result<Self> {
39        let host = connection_string
40            .split('@')
41            .next_back()
42            .context("Invalid connection string, missing credentials")?;
43        let host = host
44            .split('/')
45            .next()
46            .context("Invalid connection string, missing db path")?;
47        let protocol = if host == "db.localtest.me:4444" {
48            "http".to_string()
49        } else {
50            "https".to_string()
51        };
52
53        Ok(Self {
54            connection_string: connection_string.to_owned(),
55            client: Default::default(),
56            url: format!("{protocol}://{host}/sql"),
57        })
58    }
59
60    /// Execute a SQL query
61    pub async fn execute(&self, query: Query) -> Result<()> {
62        self.execute_raw(query, false).await?;
63        Ok(())
64    }
65
66    /// Execute a SQL query and return the raw response
67    pub async fn execute_raw(&self, sql: Query, is_select: bool) -> Result<QueryResponse> {
68        post(
69            self,
70            &self.url,
71            if is_select {
72                serde_json::json!({ "query": format!(
73                    "WITH SelectQueryRes AS (
74                        {0}
75                    )
76                    SELECT row_to_json(SelectQueryRes) as jsonb_build_object FROM SelectQueryRes;",
77                        sql.query
78                ), "params": sql.params })
79            } else {
80                serde_json::json!({ "query": sql.query, "params": sql.params })
81            },
82        )
83        .await
84    }
85
86    /// Execute a SQL transaction
87    pub async fn execute_transaction(&self, transaction: Transaction) -> Result<()> {
88        self.execute_transaction_raw(transaction).await?;
89        Ok(())
90    }
91
92    /// Execute a SQL transaction and return the raw response
93    pub async fn execute_transaction_raw(&self, sql: Transaction) -> Result<TransactionResponse> {
94        post(self, &self.url, sql).await
95    }
96
97    #[cfg(feature = "orm_beta")]
98    pub(crate) async fn execute_orm(&self, transaction: serde_json::Value) -> Result<()> {
99        self.execute_orm_raw(transaction).await?;
100        Ok(())
101    }
102
103    #[cfg(feature = "orm_beta")]
104    pub(crate) async fn execute_orm_raw(
105        &self,
106        sql: serde_json::Value,
107    ) -> Result<TransactionResponse> {
108        post(self, &self.url, sql).await
109    }
110
111    #[cfg(feature = "orm_beta")]
112    pub(crate) async fn execute_orm_raw_query(
113        &self,
114        sql: serde_json::Value,
115    ) -> Result<QueryResponse> {
116        post(self, &self.url, sql).await
117    }
118}
119
120#[cfg(test)]
121mod test {
122    use super::*;
123    use anyhow::Result;
124    use serde::{Deserialize, Serialize};
125
126    #[cfg(feature = "orm_beta")]
127    #[derive(Debug, Serialize, Deserialize, NeonTable, Clone)]
128    #[neon_table(table_name = "test", pk = "id", crate_path = "crate")]
129    pub struct TestTable {
130        pub id: Option<u64>,
131        pub name: Option<String>,
132        pub description: Option<String>,
133        #[neon_table(is_related)]
134        pub history: Vec<TestHistory>,
135        #[neon_table(is_related)]
136        pub data: Option<TestData>,
137    }
138
139    #[cfg(feature = "orm_beta")]
140    #[derive(Debug, Serialize, Deserialize, NeonTable, Clone)]
141    #[neon_table(table_name = "test_history", pk = "id", crate_path = "crate")]
142    pub struct TestHistory {
143        pub id: Option<u64>,
144        pub test_id: Option<u64>,
145        pub state: TestHistoryState,
146    }
147
148    #[cfg(feature = "orm_beta")]
149    #[derive(Debug, Serialize, Deserialize, NeonTable, Clone)]
150    #[neon_table(
151        table_name = "test_data",
152        pk = "id",
153        crate_path = "crate",
154        on_conflict = "state"
155    )]
156    pub struct TestData {
157        pub id: Option<u64>,
158        pub test_id: Option<u64>,
159        pub state: TestHistoryState,
160    }
161
162    #[derive(Debug, Serialize, Deserialize, Clone)]
163    #[serde(tag = "type", content = "data")]
164    pub enum TestHistoryState {
165        Active,
166        Closed { closed_at: String },
167        Value { int_value: i64 },
168    }
169
170    #[wstd::test]
171    pub async fn test() -> Result<()> {
172        let client = Client::new("<CONNECT_STRING>")?;
173
174        QueryBuilder::new("SELECT * FROM playing_with_neon")
175            .execute_raw(&client, true)
176            .await?;
177
178        TransactionBuilder::new()
179            .add(QueryBuilder::new("SELECT * FROM playing_with_neon").build())
180            .add(QueryBuilder::new("SELECT * FROM playing_with_neon").build())
181            .execute_raw(&client)
182            .await?;
183
184        Ok(())
185    }
186
187    #[cfg(feature = "orm_beta")]
188    #[test]
189    pub fn test_orm_insert_generation() {
190        let item = TestTable {
191            id: None,
192            name: Some("Test Name".to_string()),
193            description: None,
194            history: vec![
195                TestHistory {
196                    id: None,
197                    test_id: None,
198                    state: TestHistoryState::Active,
199                },
200                TestHistory {
201                    id: None,
202                    test_id: None,
203                    state: TestHistoryState::Closed {
204                        closed_at: "Yesterday".to_string(),
205                    },
206                },
207                TestHistory {
208                    id: None,
209                    test_id: None,
210                    state: TestHistoryState::Value { int_value: 12 },
211                },
212            ],
213            data: Some(TestData {
214                id: None,
215                test_id: None,
216                state: TestHistoryState::Active,
217            }),
218        };
219
220        let payload = orm::OrmBuilder::new().insert(item).build();
221
222        println!("Payload-Query: => {}", payload["queries"][0]["query"]);
223        println!("Payload-params: => {}", payload["queries"][0]["params"]);
224
225        let expected_sql = "WITH inserted_parent AS (INSERT INTO test (name) VALUES ($1) RETURNING id) , child_history_0_0 AS (INSERT INTO test_history (test_id, state) VALUES ((SELECT id FROM inserted_parent), $2)  RETURNING id) , child_history_1_0 AS (INSERT INTO test_history (test_id, state) VALUES ((SELECT id FROM inserted_parent), $3)  RETURNING id) , child_history_2_0 AS (INSERT INTO test_history (test_id, state) VALUES ((SELECT id FROM inserted_parent), $4)  RETURNING id) , child_data_0_0 AS (INSERT INTO test_data (test_id, state) VALUES ((SELECT id FROM inserted_parent), $5)  ON CONFLICT (state) DO NOTHING RETURNING id) SELECT id FROM inserted_parent";
226
227        let expected_params = serde_json::json!([
228            "Test Name",
229            "{\"type\":\"Active\"}",
230            "{\"data\":{\"closed_at\":\"Yesterday\"},\"type\":\"Closed\"}",
231            "{\"data\":{\"int_value\":12},\"type\":\"Value\"}",
232            "{\"type\":\"Active\"}",
233        ]);
234
235        assert_eq!(payload["queries"][0]["query"], expected_sql);
236        assert_eq!(payload["queries"][0]["params"], expected_params);
237    }
238
239    #[cfg(feature = "orm_beta")]
240    #[test]
241    pub fn test_orm_insert_with_empty_child_generation() {
242        let item = TestTable {
243            id: None,
244            name: Some("Test Name".to_string()),
245            description: None,
246            history: Vec::new(),
247            data: None,
248        };
249
250        let payload = orm::OrmBuilder::new().insert(item).build();
251
252        println!("Payload-Query: => {}", payload["queries"][0]["query"]);
253        println!("Payload-params: => {}", payload["queries"][0]["params"]);
254
255        let expected_sql = "INSERT INTO test (name) VALUES ($1)";
256
257        let expected_params = serde_json::json!(["Test Name"]);
258
259        assert_eq!(payload["queries"][0]["query"], expected_sql);
260        assert_eq!(payload["queries"][0]["params"], expected_params);
261    }
262
263    #[cfg(feature = "orm_beta")]
264    #[test]
265    fn test_orm_update_generation() {
266        let item = TestTable {
267            id: Some(42),
268            name: Some("Updated Test Name".to_string()),
269            description: None,
270            history: vec![],
271            data: None,
272        };
273
274        let payload = orm::OrmBuilder::new().update(item).build();
275
276        let expected_sql = "UPDATE test SET name = $1 WHERE id = $2";
277        let expected_params = serde_json::json!(["Updated Test Name", 42]);
278
279        let queries = &payload["queries"];
280        assert_eq!(queries[0]["query"].as_str().unwrap(), expected_sql);
281        assert_eq!(queries[0]["params"], expected_params);
282    }
283
284    #[cfg(feature = "orm_beta")]
285    #[test]
286    fn test_orm_delete_generation() {
287        use crate::orm::NeonTable;
288        let apa = TestTable::select_as_json_sql();
289        println!("{apa}");
290    }
291}