Skip to content

Commit

Permalink
feat(driver-adapters): move napi-specific code into "napi" module, pr…
Browse files Browse the repository at this point in the history
…epare empty "wasm" module
  • Loading branch information
jkomyno committed Nov 15, 2023
1 parent ea55542 commit a3d0df6
Show file tree
Hide file tree
Showing 11 changed files with 1,722 additions and 0 deletions.
16 changes: 16 additions & 0 deletions query-engine/driver-adapters/src/conversion/js_arg.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use serde::Serialize;
use serde_json::value::Value as JsonValue;

#[derive(Debug, PartialEq, Serialize)]
#[serde(untagged)]
pub enum JSArg {
Value(serde_json::Value),
Buffer(Vec<u8>),
Array(Vec<JSArg>),
}

impl From<JsonValue> for JSArg {
fn from(v: JsonValue) -> Self {
JSArg::Value(v)
}
}
7 changes: 7 additions & 0 deletions query-engine/driver-adapters/src/conversion/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
pub(crate) mod js_arg;

pub(crate) mod mysql;
pub(crate) mod postgres;
pub(crate) mod sqlite;

pub use js_arg::JSArg;
70 changes: 70 additions & 0 deletions query-engine/driver-adapters/src/napi/async_js_function.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use std::marker::PhantomData;

use napi::{
bindgen_prelude::*,
threadsafe_function::{ErrorStrategy, ThreadsafeFunction},
};

use super::{
error::{async_unwinding_panic, into_quaint_error},
result::JsResult,
};

/// Wrapper for napi-rs's ThreadsafeFunction that is aware of
/// JS drivers conventions. Performs following things:
/// - Automatically unrefs the function so it won't hold off event loop
/// - Awaits for returned Promise
/// - Unpacks JS `Result` type into Rust `Result` type and converts the error
/// into `quaint::Error`.
/// - Catches panics and converts them to `quaint:Error`
pub(crate) struct AsyncJsFunction<ArgType, ReturnType>
where
ArgType: ToNapiValue + 'static,
ReturnType: FromNapiValue + 'static,
{
threadsafe_fn: ThreadsafeFunction<ArgType, ErrorStrategy::Fatal>,
_phantom: PhantomData<ReturnType>,
}

impl<ArgType, ReturnType> AsyncJsFunction<ArgType, ReturnType>
where
ArgType: ToNapiValue + 'static,
ReturnType: FromNapiValue + 'static,
{
fn from_threadsafe_function(
mut threadsafe_fn: ThreadsafeFunction<ArgType, ErrorStrategy::Fatal>,
env: Env,
) -> napi::Result<Self> {
threadsafe_fn.unref(&env)?;

Ok(AsyncJsFunction {
threadsafe_fn,
_phantom: PhantomData,
})
}

pub(crate) async fn call(&self, arg: ArgType) -> quaint::Result<ReturnType> {
let js_result = async_unwinding_panic(async {
let promise = self
.threadsafe_fn
.call_async::<Promise<JsResult<ReturnType>>>(arg)
.await?;
promise.await
})
.await
.map_err(into_quaint_error)?;
js_result.into()
}
}

impl<ArgType, ReturnType> FromNapiValue for AsyncJsFunction<ArgType, ReturnType>
where
ArgType: ToNapiValue + 'static,
ReturnType: FromNapiValue + 'static,
{
unsafe fn from_napi_value(napi_env: napi::sys::napi_env, napi_val: napi::sys::napi_value) -> napi::Result<Self> {
let env = Env::from_raw(napi_env);
let threadsafe_fn = ThreadsafeFunction::from_napi_value(napi_env, napi_val)?;
Self::from_threadsafe_function(threadsafe_fn, env)
}
}
42 changes: 42 additions & 0 deletions query-engine/driver-adapters/src/napi/conversion.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
pub(crate) use crate::conversion::{mysql, postgres, sqlite, JSArg};

use napi::bindgen_prelude::{FromNapiValue, ToNapiValue};
use napi::NapiValue;

// FromNapiValue is the napi equivalent to serde::Deserialize.
// Note: we can safely leave this unimplemented as we don't need deserialize napi_value back to JSArg.
// However, removing this altogether would cause a compile error.
impl FromNapiValue for JSArg {
unsafe fn from_napi_value(_env: napi::sys::napi_env, _napi_value: napi::sys::napi_value) -> napi::Result<Self> {
unreachable!()
}
}

// ToNapiValue is the napi equivalent to serde::Serialize.
impl ToNapiValue for JSArg {
unsafe fn to_napi_value(env: napi::sys::napi_env, value: Self) -> napi::Result<napi::sys::napi_value> {
match value {
JSArg::Value(v) => ToNapiValue::to_napi_value(env, v),
JSArg::Buffer(bytes) => {
ToNapiValue::to_napi_value(env, napi::Env::from_raw(env).create_buffer_with_data(bytes)?.into_raw())
}
// While arrays are encodable as JSON generally, their element might not be, or may be
// represented in a different way than we need. We use this custom logic for all arrays
// to avoid having separate `JsonArray` and `BytesArray` variants in `JSArg` and
// avoid complicating the logic in `conv_params`.
JSArg::Array(items) => {
let env = napi::Env::from_raw(env);
let mut array = env.create_array(items.len().try_into().expect("JS array length must fit into u32"))?;

for (index, item) in items.into_iter().enumerate() {
let js_value = ToNapiValue::to_napi_value(env.raw(), item)?;
// TODO: NapiRaw could be implemented for sys::napi_value directly, there should
// be no need for re-wrapping; submit a patch to napi-rs and simplify here.
array.set(index as u32, napi::JsUnknown::from_raw_unchecked(env.raw(), js_value))?;
}

ToNapiValue::to_napi_value(env.raw(), array)
}
}
}
}
35 changes: 35 additions & 0 deletions query-engine/driver-adapters/src/napi/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use futures::{Future, FutureExt};
use napi::Error as NapiError;
use quaint::error::Error as QuaintError;
use std::{any::Any, panic::AssertUnwindSafe};

/// transforms a napi error into a quaint error copying the status and reason
/// properties over
pub(crate) fn into_quaint_error(napi_err: NapiError) -> QuaintError {
let status = napi_err.status.as_ref().to_owned();
let reason = napi_err.reason.clone();

QuaintError::raw_connector_error(status, reason)
}

/// catches a panic thrown during the execution of an asynchronous closure and transforms it into
/// the Error variant of a napi::Result.
pub(crate) async fn async_unwinding_panic<F, R>(fut: F) -> napi::Result<R>
where
F: Future<Output = napi::Result<R>>,
{
AssertUnwindSafe(fut)
.catch_unwind()
.await
.unwrap_or_else(panic_to_napi_err)
}

fn panic_to_napi_err<R>(panic_payload: Box<dyn Any + Send>) -> napi::Result<R> {
panic_payload
.downcast_ref::<&str>()
.map(|s| -> String { (*s).to_owned() })
.or_else(|| panic_payload.downcast_ref::<String>().map(|s| s.to_owned()))
.map(|message| Err(napi::Error::from_reason(format!("PANIC: {message}"))))
.ok_or(napi::Error::from_reason("PANIC: unknown panic".to_string()))
.unwrap()
}
10 changes: 10 additions & 0 deletions query-engine/driver-adapters/src/napi/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//! Query Engine Driver Adapters: `napi`-specific implementation.
mod async_js_function;
mod conversion;
mod error;
mod proxy;
mod queryable;
mod result;
mod transaction;
pub use queryable::{from_napi, JsQueryable};
Loading

0 comments on commit a3d0df6

Please sign in to comment.