diff --git a/Cargo.toml b/Cargo.toml index b2944c6..766170b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,16 +9,26 @@ readme = "README.md" [features] default = ["all"] -all = ["timers", "url", "console"] +all = ["timers", "url", "console", "sqlite"] -timers = [] +timers = ["tokio/time"] url = [] console = [] +sqlite = ["sqlx"] [dependencies] either = "1" log = { version = "0.4" } -rquickjs = { version = "0.6", features = ["either", "macro", "futures"] } +rquickjs = { version = "0.6", features = [ + "array-buffer", + "either", + "macro", + "futures", +] } +sqlx = { version = "0.8", default-features = false, features = [ + "sqlite", + "runtime-tokio", +], optional = true } tokio = { version = "1" } [dev-dependencies] diff --git a/src/ffi/c_string.rs b/src/ffi/c_string.rs new file mode 100644 index 0000000..7cc7fca --- /dev/null +++ b/src/ffi/c_string.rs @@ -0,0 +1,56 @@ +use std::mem; +use std::{ffi::c_char, slice, str}; + +use rquickjs::{qjs, Error, Exception, Result, String, Value}; + +#[derive(Debug)] +pub struct CString<'js> { + ptr: *const c_char, + len: usize, + value: Value<'js>, +} + +#[allow(dead_code)] +impl<'js> CString<'js> { + pub fn from_string(string: String<'js>) -> Result { + let mut len = mem::MaybeUninit::uninit(); + let ptr = unsafe { + qjs::JS_ToCStringLen( + string.ctx().as_raw().as_ptr(), + len.as_mut_ptr(), + string.as_raw(), + ) + }; + if ptr.is_null() { + // Might not ever happen but I am not 100% sure + // so just incase check it. + return Err(Error::Unknown); + } + let len = unsafe { len.assume_init() }; + Ok(Self { + ptr, + len, + value: string.into_value(), + }) + } + + pub fn as_ptr(&self) -> *const c_char { + self.ptr + } + + pub fn len(&self) -> usize { + self.len + } + + pub fn as_str(&self) -> Result<&str> { + let bytes = unsafe { slice::from_raw_parts(self.ptr as *const u8, self.len) }; + str::from_utf8(bytes) + .map_err(|_| Exception::throw_message(self.value.ctx(), "Invalid UTF-8")) + } +} + +impl<'js> Drop for CString<'js> { + fn drop(&mut self) { + unsafe { qjs::JS_FreeCString(self.value.ctx().as_raw().as_ptr(), self.ptr) }; + } +} diff --git a/src/ffi/c_vec.rs b/src/ffi/c_vec.rs new file mode 100644 index 0000000..d7be3bd --- /dev/null +++ b/src/ffi/c_vec.rs @@ -0,0 +1,35 @@ +use rquickjs::{Result, TypedArray, Value}; + +use crate::utils::result::ResultExt; + +#[derive(Debug)] +pub struct CVec<'js> { + ptr: *const u8, + len: usize, + #[allow(dead_code)] + value: Value<'js>, +} + +#[allow(dead_code)] +impl<'js> CVec<'js> { + pub fn from_array(array: TypedArray<'js, u8>) -> Result { + let raw = array.as_raw().or_throw(array.ctx())?; + Ok(Self { + ptr: raw.ptr.as_ptr(), + len: raw.len, + value: array.into_value(), + }) + } + + pub fn as_ptr(&self) -> *const u8 { + self.ptr + } + + pub fn len(&self) -> usize { + self.len + } + + pub fn as_slice(&self) -> &[u8] { + unsafe { std::slice::from_raw_parts(self.ptr, self.len) } + } +} diff --git a/src/ffi/mod.rs b/src/ffi/mod.rs new file mode 100644 index 0000000..12c85c5 --- /dev/null +++ b/src/ffi/mod.rs @@ -0,0 +1,5 @@ +pub use self::c_string::CString; +pub use self::c_vec::CVec; + +mod c_string; +mod c_vec; diff --git a/src/lib.rs b/src/lib.rs index 1893324..e9b3ec8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,7 @@ pub use self::modules::*; +mod ffi; mod modules; #[cfg(test)] mod test; +mod utils; diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 4013737..91e8cc6 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -1,5 +1,7 @@ #[cfg(feature = "console")] pub mod console; +#[cfg(feature = "sqlite")] +pub mod sqlite; #[cfg(feature = "timers")] pub mod timers; #[cfg(feature = "url")] diff --git a/src/modules/sqlite/argument.rs b/src/modules/sqlite/argument.rs new file mode 100644 index 0000000..963fdcc --- /dev/null +++ b/src/modules/sqlite/argument.rs @@ -0,0 +1,62 @@ +use rquickjs::{Ctx, Exception, FromJs, Result, TypedArray}; +use sqlx::query::Query; +use sqlx::sqlite::SqliteArguments; +use sqlx::Sqlite; + +use crate::ffi::{CString, CVec}; +use crate::utils::result::ResultExt; + +#[derive(Debug)] +pub enum Argument<'js> { + Null, + Integer(i64), + Real(f64), + Text(CString<'js>), + Blob(CVec<'js>), +} + +impl<'js> FromJs<'js> for Argument<'js> { + fn from_js(ctx: &Ctx<'js>, value: rquickjs::Value<'js>) -> Result { + if value.is_undefined() || value.is_null() { + return Ok(Argument::Null); + } else if let Some(int) = value.as_int() { + return Ok(Argument::Integer(int as i64)); + } else if let Some(big_int) = value.as_big_int() { + return Ok(Argument::Integer(big_int.clone().to_i64()?)); + } else if let Some(float) = value.as_float() { + return Ok(Argument::Real(float)); + } else if let Some(string) = value.as_string() { + return Ok(Argument::Text(CString::from_string(string.clone())?)); + } else if let Some(object) = value.as_object() { + if object.as_typed_array::().is_some() { + // Lifetime issue: https://github.com/DelSkayn/rquickjs/issues/356 + return Ok(Argument::Blob(CVec::from_array( + TypedArray::::from_value(value.clone()).or_throw(ctx)?, + )?)); + } + } + Err(Exception::throw_type( + ctx, + &["Value of type '", value.type_name(), "' is not supported"].concat(), + )) + } +} + +impl<'js> Argument<'js> { + pub fn try_bind<'q>( + &'q self, + ctx: &Ctx<'js>, + query: &mut Query<'q, Sqlite, SqliteArguments<'q>>, + ) -> Result<()> + where + 'js: 'q, + { + match self { + Argument::Null => query.try_bind::>(None).or_throw(ctx), + Argument::Integer(int) => query.try_bind(*int).or_throw(ctx), + Argument::Real(float) => query.try_bind(*float).or_throw(ctx), + Argument::Text(string) => query.try_bind(string.as_str()?).or_throw(ctx), + Argument::Blob(blob) => query.try_bind(blob.as_slice()).or_throw(ctx), + } + } +} diff --git a/src/modules/sqlite/database.rs b/src/modules/sqlite/database.rs new file mode 100644 index 0000000..757091e --- /dev/null +++ b/src/modules/sqlite/database.rs @@ -0,0 +1,113 @@ +use rquickjs::{Ctx, Result}; +use sqlx::{Executor, SqlitePool}; + +use super::Statement; +use crate::utils::result::ResultExt; + +#[rquickjs::class] +#[derive(rquickjs::class::Trace)] +pub struct Database { + #[qjs(skip_trace)] + pool: SqlitePool, +} + +impl Database { + pub fn new(pool: SqlitePool) -> Self { + Self { pool } + } +} + +#[rquickjs::methods(rename_all = "camelCase")] +impl Database { + async fn exec(&self, ctx: Ctx<'_>, sql: String) -> Result<()> { + sqlx::raw_sql(&sql) + .execute(&self.pool) + .await + .or_throw(&ctx)?; + Ok(()) + } + + async fn prepare(&self, ctx: Ctx<'_>, sql: String) -> Result { + let stmt = sqlx::Statement::to_owned(&self.pool.prepare(&sql).await.or_throw(&ctx)?); + Ok(Statement::new(stmt, self.pool.clone())) + } + + async fn close(&mut self) -> Result<()> { + self.pool.close().await; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use rquickjs::CatchResultExt; + + use crate::sqlite::SqliteModule; + use crate::test::{call_test, test_async_with, ModuleEvaluator}; + + #[tokio::test] + async fn test_database_exec() { + test_async_with(|ctx| { + Box::pin(async move { + ModuleEvaluator::eval_rust::(ctx.clone(), "sqlite") + .await + .unwrap(); + + let module = ModuleEvaluator::eval_js( + ctx.clone(), + "test", + r#" + import { open } from "sqlite"; + + export async function test() { + const db = await open({ inMemory: true }); + await db.exec("CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY, name TEXT);"); + await db.exec("INSERT INTO test (name) VALUES ('test');"); + return "ok"; + } + "#, + ) + .await + .catch(&ctx) + .unwrap(); + + let result = call_test::(&ctx, &module, ()).await; + assert_eq!(result, "ok"); + }) + }) + .await; + } + + #[tokio::test] + async fn test_database_close() { + test_async_with(|ctx| { + Box::pin(async move { + ModuleEvaluator::eval_rust::(ctx.clone(), "sqlite") + .await + .unwrap(); + + let module = ModuleEvaluator::eval_js( + ctx.clone(), + "test", + r#" + import { open } from "sqlite"; + + export async function test() { + const db = await open({ inMemory: true }); + await db.exec("CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY, name TEXT);"); + await db.close(); + return "ok"; + } + "#, + ) + .await + .catch(&ctx) + .unwrap(); + + let result = call_test::(&ctx, &module, ()).await; + assert_eq!(result, "ok"); + }) + }) + .await; + } +} diff --git a/src/modules/sqlite/error.rs b/src/modules/sqlite/error.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/sqlite/mod.rs b/src/modules/sqlite/mod.rs new file mode 100644 index 0000000..aab0212 --- /dev/null +++ b/src/modules/sqlite/mod.rs @@ -0,0 +1,41 @@ +use rquickjs::{ + function::{Async, Func}, + module::{Declarations, Exports, ModuleDef}, + Class, Ctx, Result, +}; + +use self::argument::Argument; +use self::database::Database; +use self::statement::Statement; +use self::value::Value; +use crate::utils::module::export_default; + +mod argument; +mod database; +mod error; +mod open; +mod statement; +mod value; + +pub struct SqliteModule; + +impl ModuleDef for SqliteModule { + fn declare(declare: &Declarations) -> Result<()> { + declare.declare(stringify!(Database))?; + declare.declare("open")?; + declare.declare("default")?; + + Ok(()) + } + + fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> { + export_default(ctx, exports, |default| { + Class::::define(default)?; + + default.set("open", Func::from(Async(open::open)))?; + + Ok(()) + })?; + Ok(()) + } +} diff --git a/src/modules/sqlite/open.rs b/src/modules/sqlite/open.rs new file mode 100644 index 0000000..43e1edc --- /dev/null +++ b/src/modules/sqlite/open.rs @@ -0,0 +1,61 @@ +use std::{ + sync::atomic::{AtomicUsize, Ordering}, + time::Duration, +}; + +use rquickjs::{Ctx, FromJs, Object, Result, Value}; +use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; + +use super::Database; + +static IN_MEMORY_DB_SEQ: AtomicUsize = AtomicUsize::new(0); + +pub async fn open(options: OpenOptions) -> Result { + let mut connect_options = SqliteConnectOptions::new(); + connect_options = connect_options + .foreign_keys(true) + .thread_name(|id| format!("quickjs-sqlite-worker-{id}")); + if let Some(filename) = options.filename { + connect_options = connect_options.filename(filename).create_if_missing(true); + } + if options.in_memory { + let seqno = IN_MEMORY_DB_SEQ.fetch_add(1, Ordering::Relaxed); + connect_options = connect_options + .filename(format!("file:sqlite-in-memory-{seqno}")) + .in_memory(true) + .shared_cache(true); + } + if options.wal { + connect_options = connect_options.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal); + } + + let mut pool_options = SqlitePoolOptions::new(); + pool_options = pool_options + .idle_timeout(None) + .max_lifetime(Some(Duration::from_secs(60 * 60))) + .max_connections(5) + .min_connections(0); + + let pool = pool_options.connect_with(connect_options).await.unwrap(); + Ok(Database::new(pool)) +} + +pub struct OpenOptions { + filename: Option, + in_memory: bool, + wal: bool, +} + +impl<'js> FromJs<'js> for OpenOptions { + fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result { + let obj = value.get::>()?; + let filename = obj.get("filename")?; + let in_memory = obj.get("inMemory").unwrap_or(false); + let wal = obj.get("wal").unwrap_or(true); + Ok(Self { + filename, + in_memory, + wal, + }) + } +} diff --git a/src/modules/sqlite/statement.rs b/src/modules/sqlite/statement.rs new file mode 100644 index 0000000..1eb4254 --- /dev/null +++ b/src/modules/sqlite/statement.rs @@ -0,0 +1,212 @@ +use rquickjs::function::Rest; +use rquickjs::{Ctx, Object, Result}; +use sqlx::query::Query; +use sqlx::sqlite::SqliteArguments; +use sqlx::Sqlite; +use sqlx::{sqlite::SqliteStatement, Column as _, Row as _, SqlitePool, Statement as _}; + +use crate::utils::result::ResultExt; + +use super::{Argument, Value}; + +#[rquickjs::class] +#[derive(rquickjs::class::Trace)] +pub struct Statement { + #[qjs(skip_trace)] + stmt: SqliteStatement<'static>, + #[qjs(skip_trace)] + pool: SqlitePool, +} + +impl Statement { + pub fn new(stmt: SqliteStatement<'static>, pool: SqlitePool) -> Self { + Self { stmt, pool } + } + + fn query<'js, 'q>( + &'q self, + ctx: &Ctx<'js>, + binds: &'q [Argument<'js>], + ) -> Result>> + where + 'js: 'q, + { + let mut query = self.stmt.query(); + for value in binds { + value.try_bind(ctx, &mut query)?; + } + Ok(query) + } + + fn row_to_object<'js>(ctx: &Ctx<'js>, row: &sqlx::sqlite::SqliteRow) -> Result> { + let obj = Object::new(ctx.clone())?; + for column in row.columns() { + let value = Value::try_read(ctx, column, row)?; + obj.set(column.name(), value)?; + } + Ok(obj) + } +} + +#[rquickjs::methods(rename_all = "camelCase")] +impl Statement { + async fn all<'js>( + &self, + ctx: Ctx<'js>, + anon_params: Rest>, + ) -> Result>> { + let query = self.query(&ctx, &anon_params.0)?; + + let rows = query.fetch_all(&self.pool).await.or_throw(&ctx)?; + + let mut res = Vec::with_capacity(rows.len()); + for row in rows { + let obj = Self::row_to_object(&ctx, &row)?; + res.push(obj); + } + Ok(res) + } + + async fn get<'js>( + &self, + ctx: Ctx<'js>, + anon_params: Rest>, + ) -> Result>> { + let query = self.query(&ctx, &anon_params.0)?; + + let Some(row) = query.fetch_optional(&self.pool).await.or_throw(&ctx)? else { + return Ok(None); + }; + + let obj = Self::row_to_object(&ctx, &row)?; + Ok(Some(obj)) + } + + async fn run<'js>( + &self, + ctx: Ctx<'js>, + anon_params: Rest>, + ) -> Result> { + let query = self.query(&ctx, &anon_params.0)?; + + let res = query.execute(&self.pool).await.or_throw(&ctx)?; + + let obj = Object::new(ctx.clone())?; + obj.set("changes", res.rows_affected())?; + obj.set("lastInsertRowid", res.last_insert_rowid())?; + Ok(obj) + } +} + +#[cfg(test)] +mod tests { + use rquickjs::CatchResultExt; + + use crate::sqlite::SqliteModule; + use crate::test::{call_test, test_async_with, ModuleEvaluator}; + + #[tokio::test] + async fn test_statement_all() { + test_async_with(|ctx| { + Box::pin(async move { + ModuleEvaluator::eval_rust::(ctx.clone(), "sqlite") + .await + .unwrap(); + + let module = ModuleEvaluator::eval_js( + ctx.clone(), + "test", + r#" + import { open } from "sqlite"; + + export async function test() { + const db = await open({ inMemory: true }); + await db.exec("CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY, name TEXT)"); + await db.exec("INSERT INTO test (name) VALUES ('test')"); + await db.exec("INSERT INTO test (name) VALUES ('test2')"); + const stmt = await db.prepare("SELECT * FROM test"); + const rows = await stmt.all(); + return rows[1].id; + } + "#, + ) + .await + .catch(&ctx) + .unwrap(); + + let result = call_test::(&ctx, &module, ()).await; + assert_eq!(result, 2); + }) + }) + .await; + } + + #[tokio::test] + async fn test_statement_get() { + test_async_with(|ctx| { + Box::pin(async move { + ModuleEvaluator::eval_rust::(ctx.clone(), "sqlite") + .await + .unwrap(); + + let module = ModuleEvaluator::eval_js( + ctx.clone(), + "test", + r#" + import { open } from "sqlite"; + + export async function test() { + const db = await open({ inMemory: true }); + await db.exec("CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY, name TEXT)"); + await db.exec("INSERT INTO test (name) VALUES ('test')"); + const stmt = await db.prepare("SELECT * FROM test WHERE name = ?"); + const row = await stmt.get('test'); + return row.id; + } + "#, + ) + .await + .catch(&ctx) + .unwrap(); + + let result = call_test::(&ctx, &module, ()).await; + assert_eq!(result, 1); + }) + }) + .await; + } + + #[tokio::test] + async fn test_statement_run() { + test_async_with(|ctx| { + Box::pin(async move { + ModuleEvaluator::eval_rust::(ctx.clone(), "sqlite") + .await + .unwrap(); + + let module = ModuleEvaluator::eval_js( + ctx.clone(), + "test", + r#" + import { open } from "sqlite"; + + export async function test() { + const db = await open({ inMemory: true }); + await db.exec("CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY, name TEXT)"); + const stmt = await db.prepare("INSERT INTO test (name) VALUES (?), (?)"); + const res = await stmt.run('test', 'test2'); + return res.changes; + } + "#, + ) + .await + .catch(&ctx) + .unwrap(); + + let result = call_test::(&ctx, &module, ()).await; + assert_eq!(result, 2); + }) + }) + .await; + } +} diff --git a/src/modules/sqlite/value.rs b/src/modules/sqlite/value.rs new file mode 100644 index 0000000..4280965 --- /dev/null +++ b/src/modules/sqlite/value.rs @@ -0,0 +1,51 @@ +use rquickjs::{Ctx, Exception, IntoJs, Result, String, TypedArray}; +use sqlx::sqlite::{SqliteColumn, SqliteRow}; +use sqlx::{Column as _, Decode, Row as _, TypeInfo as _, ValueRef}; + +use crate::utils::result::ResultExt; + +pub enum Value<'q> { + Null, + Integer(i64), + Real(f64), + Text(&'q str), + Blob(&'q [u8]), +} + +impl<'q, 'js> IntoJs<'js> for Value<'q> +where + 'js: 'q, +{ + fn into_js(self, ctx: &Ctx<'js>) -> Result> { + match self { + Value::Null => Ok(rquickjs::Value::new_null(ctx.clone())), + Value::Integer(int) => Ok(int.into_js(ctx)?), + Value::Real(float) => Ok(float.into_js(ctx)?), + Value::Text(s) => Ok(String::from_str(ctx.clone(), s)?.into_value()), + Value::Blob(b) => Ok(TypedArray::::new_copy(ctx.clone(), b)?.into_value()), + } + } +} + +impl<'q> Value<'q> { + pub fn try_read(ctx: &Ctx<'_>, column: &'q SqliteColumn, row: &'q SqliteRow) -> Result { + let value = row.try_get_raw(column.ordinal()).or_throw(ctx)?; + + // This is annoying since in theory sqlx can change the string representation + // but we don't really have a better way to get that information. + // Also note that only base types will be present in that call, if we want to + // get more fancy (boolean, datetime, etc) we need to use the column type info. + // See https://github.com/launchbadge/sqlx/issues/606 + match value.type_info().name() { + "NULL" => Ok(Value::Null), + "INTEGER" => Ok(Value::Integer(Decode::decode(value).or_throw(ctx)?)), + "REAL" => Ok(Value::Real(Decode::decode(value).or_throw(ctx)?)), + "TEXT" => Ok(Value::Text(Decode::decode(value).or_throw(ctx)?)), + "BLOB" => Ok(Value::Blob(Decode::decode(value).or_throw(ctx)?)), + name => Err(Exception::throw_message( + ctx, + &["Unsupported type: ", name].concat(), + )), + } + } +} diff --git a/src/test.rs b/src/test.rs index 082cd5f..5af1675 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,4 +1,10 @@ -use rquickjs::{async_with, AsyncContext, AsyncRuntime, Ctx}; +use rquickjs::{ + async_with, + function::IntoArgs, + module::{Evaluated, ModuleDef}, + promise::MaybePromise, + AsyncContext, AsyncRuntime, CatchResultExt, CaughtError, Ctx, FromJs, Function, Module, Result, +}; pub fn test_with(func: F) where @@ -22,3 +28,53 @@ where }) .await; } + +pub async fn call_test<'js, T, A>(ctx: &Ctx<'js>, module: &Module<'js, Evaluated>, args: A) -> T +where + T: FromJs<'js>, + A: IntoArgs<'js>, +{ + call_test_err(ctx, module, args).await.unwrap() +} + +pub async fn call_test_err<'js, T, A>( + ctx: &Ctx<'js>, + module: &Module<'js, Evaluated>, + args: A, +) -> std::result::Result> +where + T: FromJs<'js>, + A: IntoArgs<'js>, +{ + module + .get::<_, Function>("test") + .catch(ctx)? + .call::<_, MaybePromise>(args) + .catch(ctx)? + .into_future::() + .await + .catch(ctx) +} + +pub struct ModuleEvaluator; + +impl ModuleEvaluator { + pub async fn eval_js<'js>( + ctx: Ctx<'js>, + name: &str, + source: &str, + ) -> Result> { + let (module, module_eval) = Module::declare(ctx, name, source)?.eval()?; + module_eval.into_future::<()>().await?; + Ok(module) + } + + pub async fn eval_rust<'js, M>(ctx: Ctx<'js>, name: &str) -> Result> + where + M: ModuleDef, + { + let (module, module_eval) = Module::evaluate_def::(ctx, name)?; + module_eval.into_future::<()>().await?; + Ok(module) + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..a5fc50c --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,2 @@ +pub mod module; +pub mod result; diff --git a/src/utils/module.rs b/src/utils/module.rs new file mode 100644 index 0000000..846dc06 --- /dev/null +++ b/src/utils/module.rs @@ -0,0 +1,22 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// Source: https://github.com/awslabs/llrt/blob/07eb540a204dcdce44143220876630804f381ca6/llrt_utils/src/module.rs +use rquickjs::{module::Exports, Ctx, Object, Result, Value}; + +pub fn export_default<'js, F>(ctx: &Ctx<'js>, exports: &Exports<'js>, f: F) -> Result<()> +where + F: FnOnce(&Object<'js>) -> Result<()>, +{ + let default = Object::new(ctx.clone())?; + f(&default)?; + + for name in default.keys::() { + let name = name?; + let value: Value = default.get(&name)?; + exports.export(name, value)?; + } + + exports.export("default", default)?; + + Ok(()) +} diff --git a/src/utils/result.rs b/src/utils/result.rs new file mode 100644 index 0000000..c787463 --- /dev/null +++ b/src/utils/result.rs @@ -0,0 +1,71 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// Source: https://github.com/awslabs/llrt/blob/07eb540a204dcdce44143220876630804f381ca6/llrt_utils/src/result.rs +use std::{fmt::Write, result::Result as StdResult}; + +use rquickjs::{Ctx, Exception, Result}; + +pub trait ResultExt { + fn or_throw_msg(self, ctx: &Ctx, msg: &str) -> Result; + fn or_throw_range(self, ctx: &Ctx, msg: Option<&str>) -> Result; + fn or_throw_type(self, ctx: &Ctx, msg: Option<&str>) -> Result; + fn or_throw(self, ctx: &Ctx) -> Result; +} + +impl ResultExt for StdResult { + fn or_throw_msg(self, ctx: &Ctx, msg: &str) -> Result { + self.map_err(|e| { + let mut message = String::with_capacity(100); + message.push_str(msg); + message.push_str(". "); + write!(message, "{}", e).unwrap(); + Exception::throw_message(ctx, &message) + }) + } + + fn or_throw_range(self, ctx: &Ctx, msg: Option<&str>) -> Result { + self.map_err(|e| { + let mut message = String::with_capacity(100); + if let Some(msg) = msg { + message.push_str(msg); + message.push_str(". "); + } + write!(message, "{}", e).unwrap(); + Exception::throw_range(ctx, &message) + }) + } + + fn or_throw_type(self, ctx: &Ctx, msg: Option<&str>) -> Result { + self.map_err(|e| { + let mut message = String::with_capacity(100); + if let Some(msg) = msg { + message.push_str(msg); + message.push_str(". "); + } + write!(message, "{}", e).unwrap(); + Exception::throw_type(ctx, &message) + }) + } + + fn or_throw(self, ctx: &Ctx) -> Result { + self.map_err(|err| Exception::throw_message(ctx, &err.to_string())) + } +} + +impl ResultExt for Option { + fn or_throw_msg(self, ctx: &Ctx, msg: &str) -> Result { + self.ok_or_else(|| Exception::throw_message(ctx, msg)) + } + + fn or_throw_range(self, ctx: &Ctx, msg: Option<&str>) -> Result { + self.ok_or_else(|| Exception::throw_range(ctx, msg.unwrap_or(""))) + } + + fn or_throw_type(self, ctx: &Ctx, msg: Option<&str>) -> Result { + self.ok_or_else(|| Exception::throw_type(ctx, msg.unwrap_or(""))) + } + + fn or_throw(self, ctx: &Ctx) -> Result { + self.ok_or_else(|| Exception::throw_message(ctx, "Value is not present")) + } +} diff --git a/types/index.d.ts b/types/index.d.ts index 1cc4257..7bda7c6 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,3 +1,4 @@ /// +/// /// /// diff --git a/types/sqlite.d.ts b/types/sqlite.d.ts new file mode 100644 index 0000000..e7a09e2 --- /dev/null +++ b/types/sqlite.d.ts @@ -0,0 +1,86 @@ +declare module "sqlite" { + export type Parameter = null | number | bigint | string | Uint8Array; + export type Result = { + changes: number; + lastInsertRowid: number; + }; + + export type OpenOptions = { + /** + * The filename of the database. If the file does not exist, a new one will be created. + */ + filename?: string | undefined; + /** + * If true, the database will be opened in-memory. + * @default false + */ + in_memory?: boolean | undefined; + /** + * If true, the database will use the WAL mode. + * @default true + */ + wal?: boolean | undefined; + }; + + /** + * A SQLite database. + * + * The implementation uses a connection pool and is fully asynchronous. + * Each connection will be spawned in a worker thread. + * + * @example + * ```ts + * const db = await open({ filename: "path/to/database.sqlite" }); + * await db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT);"); + * await db.exec("INSERT INTO test (name) VALUES ('foo');"); + */ + export class Database { + /** + * This method allows one or more SQL statements to be executed without returning any results. + */ + exec(sql: string): Promise; + /** + * Compiles a SQL statement into a {@link https://www.sqlite.org/c3ref/stmt.html prepared statement}. + */ + prepare(sql: string): Statement; + } + + /** + * This class represents a single prepared statement. This class cannot be instantiated via its constructor. + * Instead, instances are created via the database.prepare() method. + */ + export class Statement { + /** + * This method executes a prepared statement and returns all results as an array of objects. + * If the prepared statement does not return any results, this method returns an empty array. + * The prepared statement {@link https://www.sqlite.org/c3ref/bind_blob.html parameters are bound} using the values in `params`. + * + * @param params The values to bind to the prepared statement. Named parameters are not supported. + */ + all(...params: Parameter[]): Promise; + /** + * This method executes a prepared statement and returns the first result as an object. + * If the prepared statement does not return any results, this method returns undefined. + * The prepared statement {@link https://www.sqlite.org/c3ref/bind_blob.html parameters are bound} using the values in params. + * + * @param params The values to bind to the prepared statement. Named parameters are not supported. + */ + get( + ...params: Parameter[] + ): Promise; + /** + * This method executes a prepared statement and returns an object summarizing the resulting changes. + * The prepared statement {@link https://www.sqlite.org/c3ref/bind_blob.html parameters are bound} using the values in params. + * + * @param params The values to bind to the prepared statement. Named parameters are not supported. + */ + run(...params: Parameter[]): Promise; + } + + /** + * Open a SQLite database. + * + * @param options The options to open the database. + */ + export function open(options: OpenOptions): Promise; +}