diff --git a/README.md b/README.md index 43a82b83..2dcffd77 100644 --- a/README.md +++ b/README.md @@ -289,6 +289,46 @@ pub async fn main(message_batch: MessageBatch, env: Env, _ctx: Context) } ``` +## D1 Databases + +### Enabling D1 databases +As D1 databases are in alpha, you'll need to enable the `d1` feature on the `worker` crate. + +```toml +worker = { version = "x.y.z", features = ["d1"] } +``` + +### Example usage +```rust +use worker::*; + +#[derive(Deserialize)] +struct Thing { + thing_id: String, + desc: String, + num: u32, +} + +#[event(fetch, respond_with_errors)] +pub async fn main(request: Request, env: Env, _ctx: Context) -> Result { + Router::new() + .get_async("/:id", |_, ctx| async move { + let id = ctx.param("id").unwrap()?; + let d1 = ctx.env.d1("things-db")?; + let statement = d1.prepare("SELECT * FROM things WHERE thing_id = ?1"); + let query = statement.bind(&[id])?; + let result = query.first::(None).await?; + match result { + Some(thing) => Response::from_json(&thing), + None => Response::error("Not found", 404), + } + }) + .run(request, env) + .await +} +``` + + # Notes and FAQ It is exciting to see how much is possible with a framework like this, by expanding the options diff --git a/worker-sandbox/Cargo.toml b/worker-sandbox/Cargo.toml index 2a3a3191..5d63cb51 100644 --- a/worker-sandbox/Cargo.toml +++ b/worker-sandbox/Cargo.toml @@ -26,7 +26,7 @@ http = "0.2.8" regex = "1.5.6" serde = { version = "1.0.137", features = ["derive"] } serde_json = "1.0.81" -worker = { path = "../worker", version = "0.0.16", features= ["queue"] } +worker = { path = "../worker", version = "0.0.16", features= ["d1", "queue"] } futures-channel = "0.3.21" futures-util = { version = "0.3.21", default-features = false } rand = "0.8.5" diff --git a/worker-sandbox/src/lib.rs b/worker-sandbox/src/lib.rs index 3f160fa0..7caaf609 100644 --- a/worker-sandbox/src/lib.rs +++ b/worker-sandbox/src/lib.rs @@ -54,6 +54,21 @@ pub struct SomeSharedData { regex: regex::Regex, } + +#[derive(Deserialize, Serialize, Eq, PartialEq, Debug)] +pub struct Person { + pub id: i64, + pub name: String, + pub age: i32, +} + +#[derive(Deserialize, Serialize)] +pub struct CreatePerson { + pub name: String, + pub age: i32, +} + + fn handle_a_request(req: Request, _ctx: RouteContext) -> Result { Response::ok(format!( "req at: {}, located at: {:?}, within: {}", @@ -693,6 +708,74 @@ pub async fn main(req: Request, env: Env, _ctx: worker::Context) -> Result = guard.clone(); Response::from_json(&messages) }) + .post_async("/d1/exec", |mut req, ctx| async move { + let d1 = ctx.env.d1("DB")?; + let query = req.text().await?; + let exec_result = d1.exec(&query).await; + match exec_result { + Ok(result) => { + let count = result.count().unwrap_or(u32::MAX); + Response::ok(format!("{}", count)) + } + Err(err) => Response::error(format!("Exec failed - {}", err), 500) + } + }) + .get_async("/d1/people", |_req, ctx| async move { + let d1: Database = ctx.env.d1("DB")?; + let people: Vec = d1.prepare("select * from people;").all().await?.results()?; + Response::ok(serde_json::to_string(&people).unwrap()) + }) + .get_async("/d1/people/:id", |_req, ctx| async move { + let id = ctx.param("id").unwrap().parse::().unwrap(); + let d1: Database = ctx.env.d1("DB")?; + + match d1.prepare("select * from people where id = ?;") + .bind(&[id.to_string().into()]) + .unwrap() + .first::(None) + .await + .unwrap() { + Some(person) => Response::ok(serde_json::to_string(&person).unwrap()), + None => Response::error("Not found", 404), + } + }) + .post_async("/d1/people", |mut req, ctx| async move { + let create_person: CreatePerson = serde_json::from_str(req.text().await?.as_str()).unwrap(); + let d1: Database = ctx.env.d1("DB")?; + let new_person = d1.prepare("insert into people (name, age) values(?, ?) returning *;") + .bind(&[create_person.name.to_string().into(), create_person.age.into()]) + .unwrap() + .first::(None) + .await? + .unwrap(); + + Response::ok(serde_json::to_string(&new_person).unwrap()) + }) + .delete_async("/d1/people/:id", |_req, ctx| async move { + let id = ctx.param("id").unwrap().parse::().unwrap(); + let d1: Database = ctx.env.d1("DB")?; + + d1.prepare("delete from people where id = ?;") + .bind(&[id.to_string().into()]) + .unwrap() + .run() + .await + .unwrap(); + + Response::ok("") + }) + .post_async("/d1/people/:id", |mut req, ctx| async move { + let d1: Database = ctx.env.d1("DB")?; + let update_person: Person = serde_json::from_str(req.text().await?.as_str()).unwrap(); + let id = ctx.param("id").unwrap().parse::().unwrap(); + d1.prepare("update people set name = ?, age = ? where id = ?") + .bind(&[update_person.name.to_string().into(), update_person.age.into(), id.to_string().into()]) + .unwrap() + .run() + .await?; + + Response::ok("") + }) .get_async("/r2/list-empty", r2::list_empty) .get_async("/r2/list", r2::list) .get_async("/r2/get-empty", r2::get_empty) diff --git a/worker-sandbox/tests/d1.rs b/worker-sandbox/tests/d1.rs new file mode 100644 index 00000000..885442b2 --- /dev/null +++ b/worker-sandbox/tests/d1.rs @@ -0,0 +1,115 @@ +use worker_sandbox::{CreatePerson, Person}; + +use crate::util::{expect_wrangler, get, post, delete}; + +mod util; + +fn drop_table() { + let response = post("d1/exec", |rb| rb.body("drop table if exists people;")); + + assert!(response.status().is_success()); +} + +fn create_table() { + let query = "CREATE TABLE IF NOT EXISTS people ( \ + id INTEGER PRIMARY KEY, \ + name TEXT NOT NULL, \ + age INTEGER NOT NULL)"; + let response = post("d1/exec", |rb| rb.body(query)); + assert!(response.status().is_success()); +} + +fn setup() { + drop_table(); + create_table(); +} + +#[test] +fn d1_create_table() { + expect_wrangler(); + setup(); +} + +#[test] +fn d1_insert_prepare_first() { + expect_wrangler(); + setup(); + let person = create_person(); + + assert_eq!("Ada LoveLace", person.name); + assert_eq!(32, person.age); +} + +#[test] +fn d1_select_prepare_all() { + expect_wrangler(); + setup(); + let person = create_person(); + + let response = get("d1/people", |rb| rb); + let people: Vec = serde_json::from_str(response.text().unwrap().as_str()).unwrap(); + + assert!(people.contains(&person)); +} + +#[test] +fn d1_select_prepare_first() { + expect_wrangler(); + setup(); + let person = create_person(); + + let response = get(format!("d1/people/{}", person.id).as_str(), |rb| rb); + let person_by_id: Person = serde_json::from_str(response.text().unwrap().as_str()).unwrap(); + + assert_eq!(person_by_id, person); +} + +#[test] +fn d1_delete_prepare_run() { + expect_wrangler(); + setup(); + let person = create_person(); + let response = delete(format!("d1/people/{}", person.id).as_str(), |rb| rb); + + if response.status().is_success() { + let response = get("d1/people", |rb| rb); + let people: Vec = serde_json::from_str(response.text().unwrap().as_str()).unwrap(); + assert!(!people.contains(&person)); + } else { + panic!("failed to delete person"); + } +} + +#[test] +fn d1_update_prepare() { + expect_wrangler(); + setup(); + let og_person = create_person(); + + let update_person = Person { + id: og_person.id, + age: 100, + name: "Ada L".to_string() + }; + + post(format!("d1/people/{}", update_person.id).as_str(), |rb| rb.body(serde_json::to_string(&update_person).unwrap())); + + let response = get("d1/people", |rb| rb); + let people: Vec = serde_json::from_str(response.text().unwrap().as_str()).unwrap(); + assert!(people.contains(&update_person)); + assert!(!people.contains(&og_person)); +} + +fn create_person() -> Person { + let new_person = CreatePerson { + name: "Ada LoveLace".to_string(), + age: 32, + }; + let response = post("d1/people", |rb| rb.body(serde_json::to_string(&new_person).unwrap())); + let person: Person = serde_json::from_str(response.text().unwrap().as_str()).unwrap(); + person +} + + + + diff --git a/worker-sandbox/wrangler.toml b/worker-sandbox/wrangler.toml index fb84a81d..dda67b3f 100644 --- a/worker-sandbox/wrangler.toml +++ b/worker-sandbox/wrangler.toml @@ -5,8 +5,8 @@ compatibility_date = "2022-09-12" # required compatibility_flags = ["streams_enable_constructors"] kv_namespaces = [ - { binding = "SOME_NAMESPACE", id = "", preview_id = "" }, - { binding = "FILE_SIZES", id = "", preview_id = "" }, + { binding = "SOME_NAMESPACE", id = ".", preview_id = "." }, + { binding = "FILE_SIZES", id = ".", preview_id = "." }, ] vars = { SOME_VARIABLE = "some value" } @@ -21,6 +21,12 @@ remote-service = "./remote-service" [durable_objects] bindings = [{ name = "COUNTER", class_name = "Counter" }, { name = "ALARM", class_name = "AlarmObject" }] +[[d1_databases]] +binding = 'DB' +database_name = 'my_db' +database_id = '.' +preview_database_id = '.' + [[queues.consumers]] queue = "my_queue" @@ -29,22 +35,22 @@ bindings = [{ name = "COUNTER", class_name = "Counter" }, { name = "ALARM", clas binding = "my_queue" [[r2_buckets]] binding = 'EMPTY_BUCKET' -bucket_name = '' +bucket_name = '.' preview_bucket_name = 'empty_bucket' [[r2_buckets]] binding = 'PUT_BUCKET' -bucket_name = '' +bucket_name = '.' preview_bucket_name = 'put_bucket' [[r2_buckets]] binding = 'SEEDED_BUCKET' -bucket_name = '' +bucket_name = '.' preview_bucket_name = 'seeded_bucket' [[r2_buckets]] binding = 'DELETE_BUCKET' -bucket_name = '' +bucket_name = '.' preview_bucket_name = 'delete_bucket' [build] diff --git a/worker-sys/Cargo.toml b/worker-sys/Cargo.toml index b65e7faf..0199f9fb 100644 --- a/worker-sys/Cargo.toml +++ b/worker-sys/Cargo.toml @@ -41,4 +41,5 @@ features = [ ] [features] +d1 = [] queue = [] diff --git a/worker-sys/src/d1.rs b/worker-sys/src/d1.rs new file mode 100644 index 00000000..50a3d4d0 --- /dev/null +++ b/worker-sys/src/d1.rs @@ -0,0 +1,75 @@ +use ::js_sys::Object; +use wasm_bindgen::prelude::*; + +use js_sys::{Array, Promise}; + +#[wasm_bindgen] +extern "C" { + #[derive(Debug, Clone)] + pub type D1Result; + + #[wasm_bindgen(structural, method, getter, js_name=results)] + pub fn results(this: &D1Result) -> Option; + + #[wasm_bindgen(structural, method, getter, js_name=success)] + pub fn success(this: &D1Result) -> bool; + + #[wasm_bindgen(structural, method, getter, js_name=error)] + pub fn error(this: &D1Result) -> Option; + + #[wasm_bindgen(structural, method, getter, js_name=meta)] + pub fn meta(this: &D1Result) -> Object; +} + +#[wasm_bindgen] +extern "C" { + #[derive(Debug, Clone)] + pub type D1ExecResult; + + #[wasm_bindgen(structural, method, getter, js_name=count)] + pub fn count(this: &D1ExecResult) -> Option; + + #[wasm_bindgen(structural, method, getter, js_name=duration)] + pub fn duration(this: &D1ExecResult) -> Option; +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(extends=::js_sys::Object, js_name=D1Database)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type D1Database; + + #[wasm_bindgen(structural, method, js_class=D1Database, js_name=prepare)] + pub fn prepare(this: &D1Database, query: &str) -> D1PreparedStatement; + + #[wasm_bindgen(structural, method, js_class=D1Database, js_name=dump)] + pub fn dump(this: &D1Database) -> Promise; + + #[wasm_bindgen(structural, method, js_class=D1Database, js_name=batch)] + pub fn batch(this: &D1Database, statements: Array) -> Promise; + + #[wasm_bindgen(structural, method, js_class=D1Database, js_name=exec)] + pub fn exec(this: &D1Database, query: &str) -> Promise; +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(extends=::js_sys::Object, js_name=D1PreparedStatement)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type D1PreparedStatement; + + #[wasm_bindgen(structural, method, catch, variadic, js_class=D1PreparedStatement, js_name=bind)] + pub fn bind(this: &D1PreparedStatement, values: Array) -> Result; + + #[wasm_bindgen(structural, method, js_class=D1PreparedStatement, js_name=first)] + pub fn first(this: &D1PreparedStatement, col_name: Option<&str>) -> Promise; + + #[wasm_bindgen(structural, method, js_class=D1PreparedStatement, js_name=run)] + pub fn run(this: &D1PreparedStatement) -> Promise; + + #[wasm_bindgen(structural, method, js_class=D1PreparedStatement, js_name=all)] + pub fn all(this: &D1PreparedStatement) -> Promise; + + #[wasm_bindgen(structural, method, js_class=D1PreparedStatement, js_name=raw)] + pub fn raw(this: &D1PreparedStatement) -> Promise; +} diff --git a/worker-sys/src/types.rs b/worker-sys/src/types.rs index 899056bd..702f7510 100644 --- a/worker-sys/src/types.rs +++ b/worker-sys/src/types.rs @@ -1,4 +1,6 @@ mod context; +#[cfg(feature = "d1")] +mod d1; mod durable_object; mod dynamic_dispatcher; mod fetcher; @@ -12,6 +14,8 @@ mod tls_client_auth; mod websocket_pair; pub use context::*; +#[cfg(feature = "d1")] +pub use d1::*; pub use durable_object::*; pub use dynamic_dispatcher::*; pub use fetcher::*; diff --git a/worker-sys/src/types/d1.rs b/worker-sys/src/types/d1.rs new file mode 100644 index 00000000..1707b16c --- /dev/null +++ b/worker-sys/src/types/d1.rs @@ -0,0 +1,9 @@ +mod database; +mod exec_result; +mod prepared_statement; +mod result; + +pub use database::*; +pub use exec_result::*; +pub use prepared_statement::*; +pub use result::*; diff --git a/worker-sys/src/types/d1/database.rs b/worker-sys/src/types/d1/database.rs new file mode 100644 index 00000000..19a5b1fa --- /dev/null +++ b/worker-sys/src/types/d1/database.rs @@ -0,0 +1,24 @@ +use wasm_bindgen::prelude::*; + +use js_sys::{Array, Promise}; + +use crate::types::d1::prepared_statement::D1PreparedStatement; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(extends=js_sys::Object, js_name=D1Database)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type D1Database; + + #[wasm_bindgen(method, js_class=D1Database)] + pub fn prepare(this: &D1Database, query: &str) -> D1PreparedStatement; + + #[wasm_bindgen(method, js_class=D1Database)] + pub fn dump(this: &D1Database) -> Promise; + + #[wasm_bindgen(method, js_class=D1Database)] + pub fn batch(this: &D1Database, statements: Array) -> Promise; + + #[wasm_bindgen(method, js_class=D1Database)] + pub fn exec(this: &D1Database, query: &str) -> Promise; +} diff --git a/worker-sys/src/types/d1/exec_result.rs b/worker-sys/src/types/d1/exec_result.rs new file mode 100644 index 00000000..304be41c --- /dev/null +++ b/worker-sys/src/types/d1/exec_result.rs @@ -0,0 +1,14 @@ +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(extends=js_sys::Object)] + #[derive(Debug, Clone)] + pub type D1ExecResult; + + #[wasm_bindgen(method, getter)] + pub fn count(this: &D1ExecResult) -> Option; + + #[wasm_bindgen(method, getter)] + pub fn duration(this: &D1ExecResult) -> Option; +} diff --git a/worker-sys/src/types/d1/prepared_statement.rs b/worker-sys/src/types/d1/prepared_statement.rs new file mode 100644 index 00000000..708d49e1 --- /dev/null +++ b/worker-sys/src/types/d1/prepared_statement.rs @@ -0,0 +1,25 @@ +use wasm_bindgen::prelude::*; + +use js_sys::{Array, Promise}; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(extends=js_sys::Object, js_name=D1PreparedStatement)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type D1PreparedStatement; + + #[wasm_bindgen(method, catch, variadic, js_class=D1PreparedStatement)] + pub fn bind(this: &D1PreparedStatement, values: Array) -> Result; + + #[wasm_bindgen(method, js_class=D1PreparedStatement)] + pub fn first(this: &D1PreparedStatement, col_name: Option<&str>) -> Promise; + + #[wasm_bindgen(method, js_class=D1PreparedStatement)] + pub fn run(this: &D1PreparedStatement) -> Promise; + + #[wasm_bindgen(method, js_class=D1PreparedStatement)] + pub fn all(this: &D1PreparedStatement) -> Promise; + + #[wasm_bindgen(method, js_class=D1PreparedStatement)] + pub fn raw(this: &D1PreparedStatement) -> Promise; +} diff --git a/worker-sys/src/types/d1/result.rs b/worker-sys/src/types/d1/result.rs new file mode 100644 index 00000000..a303fe94 --- /dev/null +++ b/worker-sys/src/types/d1/result.rs @@ -0,0 +1,23 @@ +use ::js_sys::Object; +use wasm_bindgen::prelude::*; + +use js_sys::Array; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(extends=js_sys::Object)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub type D1Result; + + #[wasm_bindgen(method, getter)] + pub fn results(this: &D1Result) -> Option; + + #[wasm_bindgen(method, getter)] + pub fn success(this: &D1Result) -> bool; + + #[wasm_bindgen(method, getter, js_name=error)] + pub fn error(this: &D1Result) -> Option; + + #[wasm_bindgen(method, getter, js_name=meta)] + pub fn meta(this: &D1Result) -> Object; +} diff --git a/worker-sys/src/types/durable_object/namespace.rs b/worker-sys/src/types/durable_object/namespace.rs index f75453a7..ac5654ce 100644 --- a/worker-sys/src/types/durable_object/namespace.rs +++ b/worker-sys/src/types/durable_object/namespace.rs @@ -1,6 +1,6 @@ use wasm_bindgen::prelude::*; -use crate::types::{DurableObject, DurableObjectId}; +use crate::types::durable_object::{DurableObject, DurableObjectId}; #[wasm_bindgen] extern "C" { diff --git a/worker-sys/src/types/durable_object/state.rs b/worker-sys/src/types/durable_object/state.rs index 2570846e..2b81f6b1 100644 --- a/worker-sys/src/types/durable_object/state.rs +++ b/worker-sys/src/types/durable_object/state.rs @@ -1,6 +1,6 @@ use wasm_bindgen::prelude::*; -use crate::types::{DurableObjectId, DurableObjectStorage}; +use crate::types::durable_object::{DurableObjectId, DurableObjectStorage}; #[wasm_bindgen] extern "C" { diff --git a/worker-sys/src/types/durable_object/storage.rs b/worker-sys/src/types/durable_object/storage.rs index 66ea7f7f..2c9d98f6 100644 --- a/worker-sys/src/types/durable_object/storage.rs +++ b/worker-sys/src/types/durable_object/storage.rs @@ -1,6 +1,6 @@ use wasm_bindgen::prelude::*; -use crate::types::DurableObjectTransaction; +use crate::types::durable_object::DurableObjectTransaction; #[wasm_bindgen] extern "C" { diff --git a/worker/Cargo.toml b/worker/Cargo.toml index 4d828438..b319a086 100644 --- a/worker/Cargo.toml +++ b/worker/Cargo.toml @@ -41,3 +41,4 @@ features = [ [features] queue = ["worker-macros/queue", "worker-sys/queue"] +d1 = ["worker-sys/d1"] diff --git a/worker/src/d1/macros.rs b/worker/src/d1/macros.rs new file mode 100644 index 00000000..e79fa67b --- /dev/null +++ b/worker/src/d1/macros.rs @@ -0,0 +1,38 @@ +/// Prepare a D1 query from the provided D1Database, query string, and optional query parameters. +/// +/// Any parameter provided is required to implement [`serde::Serialize`] to be used. +/// +/// Using [`query`] is equivalent to using db.prepare('').bind('') in Javascript. +/// +/// # Example +/// +/// ``` +/// let query = worker::query!( +/// &d1, +/// "SELECT * FROM things WHERE num > ?1 AND num < ?2", +/// &min, +/// &max, +/// )?; +/// ``` +#[macro_export] +macro_rules! query { + // rule for simple queries + ($db:expr, $query:expr) => { + $crate::d1::Database::prepare($db, $query) + }; + // rule for parameterized queries + ($db:expr, $query:expr, $($args:expr),* $(,)?) => {{ + || -> $crate::Result<$crate::d1::PreparedStatement> { + let prepared = $crate::d1::Database::prepare($db, $query); + + // D1 doesn't support taking in undefined values, so we translate these missing values to NULL. + let serializer = $crate::d1::serde_wasm_bindgen::Serializer::new().serialize_missing_as_null(true); + let bindings = &[$( + ::serde::ser::Serialize::serialize(&$args, &serializer) + .map_err(|e| $crate::Error::Internal(e.into()))? + ),*]; + + $crate::d1::PreparedStatement::bind(prepared, bindings) + }() + }}; +} diff --git a/worker/src/d1/mod.rs b/worker/src/d1/mod.rs new file mode 100644 index 00000000..c0cb520d --- /dev/null +++ b/worker/src/d1/mod.rs @@ -0,0 +1,228 @@ +use js_sys::Array; +use js_sys::ArrayBuffer; +use js_sys::Uint8Array; +use serde::Deserialize; +use wasm_bindgen::{JsCast, JsValue}; +use wasm_bindgen_futures::JsFuture; +use worker_sys::{ + D1Database as D1DatabaseSys, D1ExecResult, D1PreparedStatement as D1PreparedStatementSys, + D1Result as D1ResultSys, +}; + +use crate::env::EnvBinding; +use crate::Error; +use crate::Result; + +pub use serde_wasm_bindgen; + +pub mod macros; + +// A D1 Database. +pub struct Database(D1DatabaseSys); + +impl Database { + /// Prepare a query statement from a query string. + pub fn prepare>(&self, query: T) -> PreparedStatement { + self.0.prepare(&query.into()).into() + } + + /// Dump the data in the database to a `Vec`. + pub async fn dump(&self) -> Result> { + let array_buffer = JsFuture::from(self.0.dump()).await?; + let array_buffer = array_buffer.dyn_into::()?; + let array = Uint8Array::new(&array_buffer); + let mut vec = Vec::with_capacity(array.length() as usize); + array.copy_to(&mut vec); + Ok(vec) + } + + /// Batch execute one or more statements against the database. + /// + /// Returns the results in the same order as the provided statements. + pub async fn batch(&self, statements: Vec) -> Result> { + let statements = statements.into_iter().map(|s| s.0).collect::(); + let results = JsFuture::from(self.0.batch(statements)).await?; + let results = results.dyn_into::()?; + let mut vec = Vec::with_capacity(results.length() as usize); + for result in results.iter() { + let result = result.dyn_into::()?; + vec.push(D1Result(result)); + } + Ok(vec) + } + + /// Execute one or more queries directly against the database. + /// + /// The input can be one or multiple queries separated by `\n`. + /// + /// # Considerations + /// + /// This method can have poorer performance (prepared statements can be reused + /// in some cases) and, more importantly, is less safe. Only use this + /// method for maintenance and one-shot tasks (example: migration jobs). + /// + /// If an error occurs, an exception is thrown with the query and error + /// messages, execution stops and further statements are not executed. + pub async fn exec(&self, query: &str) -> Result { + let result = JsFuture::from(self.0.exec(query)).await?; + Ok(result.into()) + } +} + +impl EnvBinding for Database { + const TYPE_NAME: &'static str = "D1Database"; + + // Workaround for Miniflare D1 Beta + fn get(val: JsValue) -> Result { + let obj = js_sys::Object::from(val); + if obj.constructor().name() == Self::TYPE_NAME || obj.constructor().name() == "BetaDatabase" + { + Ok(obj.unchecked_into()) + } else { + Err(format!( + "Binding cannot be cast to the type {} from {}", + Self::TYPE_NAME, + obj.constructor().name() + ) + .into()) + } + } +} + +impl JsCast for Database { + fn instanceof(val: &JsValue) -> bool { + val.is_instance_of::() + } + + fn unchecked_from_js(val: JsValue) -> Self { + Self(val.into()) + } + + fn unchecked_from_js_ref(val: &JsValue) -> &Self { + unsafe { &*(val as *const JsValue as *const Self) } + } +} + +impl From for JsValue { + fn from(database: Database) -> Self { + JsValue::from(database.0) + } +} + +impl AsRef for Database { + fn as_ref(&self) -> &JsValue { + &self.0 + } +} + +impl From for Database { + fn from(inner: D1DatabaseSys) -> Self { + Self(inner) + } +} + +// A D1 prepared query statement. +pub struct PreparedStatement(D1PreparedStatementSys); + +impl PreparedStatement { + /// Bind one or more parameters to the statement. + /// Consumes the old statement and returns a new statement with the bound parameters. + /// + /// D1 follows the SQLite convention for prepared statements parameter binding. + /// + /// # Considerations + /// + /// Supports Ordered (?NNNN) and Anonymous (?) parameters - named parameters are currently not supported. + /// + pub fn bind(self, values: &[JsValue]) -> Result { + let array: Array = values.iter().collect::(); + + match self.0.bind(array) { + Ok(stmt) => Ok(PreparedStatement(stmt)), + Err(err) => Err(Error::from(err)), + } + } + + /// Return the first row of results. + /// + /// If `col_name` is `Some`, returns that single value, otherwise returns the entire object. + /// + /// If the query returns no rows, then this will return `None`. + /// + /// If the query returns rows, but column does not exist, then this will return an `Err`. + pub async fn first(&self, col_name: Option<&str>) -> Result> + where + T: for<'a> Deserialize<'a>, + { + let js_value = JsFuture::from(self.0.first(col_name)).await?; + let value = serde_wasm_bindgen::from_value(js_value)?; + Ok(value) + } + + /// Executes a query against the database but only return metadata. + pub async fn run(&self) -> Result { + let result = JsFuture::from(self.0.run()).await?; + Ok(D1Result(result.into())) + } + + /// Executes a query against the database and returns all rows and metadata. + pub async fn all(&self) -> Result { + let result = JsFuture::from(self.0.all()).await?; + Ok(D1Result(result.into())) + } + + /// Executes a query against the database and returns a `Vec` of rows instead of objects. + pub async fn raw(&self) -> Result>> + where + T: for<'a> Deserialize<'a>, + { + let result = JsFuture::from(self.0.raw()).await?; + let result = result.dyn_into::()?; + let mut vec = Vec::with_capacity(result.length() as usize); + for value in result.iter() { + let value = serde_wasm_bindgen::from_value(value)?; + vec.push(value); + } + Ok(vec) + } +} + +impl From for PreparedStatement { + fn from(inner: D1PreparedStatementSys) -> Self { + Self(inner) + } +} + +// The result of a D1 query execution. +pub struct D1Result(D1ResultSys); + +impl D1Result { + /// Returns `true` if the result indicates a success, otherwise `false`. + pub fn success(&self) -> bool { + self.0.success() + } + + /// Return the error contained in this result. + /// + /// Returns `None` if the result indicates a success. + pub fn error(&self) -> Option { + self.0.error() + } + + /// Retrieve the collection of result objects, or an `Err` if an error occurred. + pub fn results(&self) -> Result> + where + T: for<'a> Deserialize<'a>, + { + if let Some(results) = self.0.results() { + let mut vec = Vec::with_capacity(results.length() as usize); + for result in results.iter() { + let result = serde_wasm_bindgen::from_value(result)?; + vec.push(result); + } + Ok(vec) + } else { + Ok(Vec::new()) + } + } +} diff --git a/worker/src/env.rs b/worker/src/env.rs index aa66a8af..2ed59e33 100644 --- a/worker/src/env.rs +++ b/worker/src/env.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "d1")] +use crate::d1::Database; use crate::error::Error; #[cfg(feature = "queue")] use crate::Queue; @@ -69,6 +71,12 @@ impl Env { pub fn bucket(&self, binding: &str) -> Result { self.get_binding(binding) } + + /// Access a D1 Database by the binding name configured in your wrangler.toml file. + #[cfg(feature = "d1")] + pub fn d1(&self, binding: &str) -> Result { + self.get_binding(binding) + } } pub trait EnvBinding: Sized + JsCast { diff --git a/worker/src/error.rs b/worker/src/error.rs index d4c1e921..1d1bd513 100644 --- a/worker/src/error.rs +++ b/worker/src/error.rs @@ -60,12 +60,18 @@ impl std::fmt::Display for Error { impl std::error::Error for Error {} +// Not sure if the changes I've made here are good or bad... impl From for Error { fn from(v: JsValue) -> Self { - match v - .as_string() - .or_else(|| v.dyn_ref::().map(|e| e.to_string().into())) - { + match v.as_string().or_else(|| { + v.dyn_ref::().map(|e| { + format!( + "Error: {} - Cause: {}", + e.to_string(), + e.cause().as_string().unwrap_or(String::from("N/A")) + ) + }) + }) { Some(s) => Self::JsError(s), None => Self::Internal(v), } diff --git a/worker/src/lib.rs b/worker/src/lib.rs index c4e2f635..f33a9e12 100644 --- a/worker/src/lib.rs +++ b/worker/src/lib.rs @@ -25,6 +25,8 @@ pub use crate::abort::*; pub use crate::cache::{Cache, CacheDeletionOutcome}; pub use crate::context::Context; pub use crate::cors::Cors; +#[cfg(feature = "d1")] +pub use crate::d1::*; pub use crate::date::{Date, DateInit}; pub use crate::delay::Delay; pub use crate::durable::*; @@ -52,6 +54,9 @@ mod cache; mod cf; mod context; mod cors; +// Require pub module for macro export +#[cfg(feature = "d1")] +pub mod d1; mod date; mod delay; pub mod durable;