Skip to main content

sqlx_cloudflare_core/
js.rs

1//! The conversions between JavaScript values and this crate's.
2//!
3//! Each driver's own `js` module talks to its binding; these are the
4//! conversions both need on either side of that call -- one point in each
5//! direction, so the two backends cannot drift apart on what a value means.
6
7use js_sys::{Array, ArrayBuffer, Uint8Array};
8use sqlx_core::error::Error;
9use wasm_bindgen::prelude::*;
10use wasm_bindgen::JsCast;
11
12use crate::{safe_integer, DatabaseError, Value};
13
14/// `values` as a JavaScript array, in placeholder order.
15///
16/// # Errors
17///
18/// When an integer is outside ±(2^53 − 1).
19pub fn to_js_array(values: &[Value]) -> Result<Array, Error> {
20    values.iter().map(to_js).collect()
21}
22
23/// A bound parameter as the JavaScript value the binding expects.
24///
25/// # Errors
26///
27/// When an integer is outside ±(2^53 − 1).
28pub fn to_js(value: &Value) -> Result<JsValue, Error> {
29    Ok(match value {
30        Value::Null => JsValue::NULL,
31        // Checked again here, not only in `Encode for i64`: a third-party
32        // `Encode` impl can push any `Integer` it likes.
33        Value::Integer(value) => {
34            #[allow(
35                clippy::cast_precision_loss,
36                reason = "`safe_integer` has just checked the conversion is exact"
37            )]
38            let value = safe_integer(*value).map_err(Error::Encode)? as f64;
39            JsValue::from_f64(value)
40        }
41        Value::Real(value) => JsValue::from_f64(*value),
42        Value::Text(value) => JsValue::from_str(value),
43        // An `ArrayBuffer`, which both backends store as BLOB. A fresh
44        // `Uint8Array` owns a buffer of exactly its length.
45        Value::Blob(value) => Uint8Array::from(value.as_slice()).buffer().into(),
46    })
47}
48
49/// A JavaScript value from a result row, as a [`Value`].
50///
51/// # Errors
52///
53/// When the value is of a JavaScript type neither backend returns.
54pub fn from_js(value: &JsValue) -> Result<Value, Error> {
55    Ok(if value.is_null() || value.is_undefined() {
56        Value::Null
57    } else if let Some(number) = value.as_f64() {
58        Value::from_number(number)
59    } else if let Some(text) = value.as_string() {
60        Value::Text(text)
61    } else if let Some(boolean) = value.as_bool() {
62        Value::Integer(i64::from(boolean))
63    } else if let Some(buffer) = value.dyn_ref::<ArrayBuffer>() {
64        Value::Blob(Uint8Array::new(buffer).to_vec())
65    } else if let Some(bytes) = value.dyn_ref::<Uint8Array>() {
66        Value::Blob(bytes.to_vec())
67    } else if let Some(numbers) = value.dyn_ref::<Array>() {
68        // D1 has returned BLOBs as plain arrays of byte values.
69        Value::Blob(bytes_from_numbers(numbers)?)
70    } else {
71        return Err(Error::Decode(
72            format!("a value of an unexpected JavaScript type came back: {value:?}").into(),
73        ));
74    })
75}
76
77fn bytes_from_numbers(numbers: &Array) -> Result<Vec<u8>, Error> {
78    numbers
79        .iter()
80        .map(|number| {
81            number
82                .as_f64()
83                .filter(|number| number.fract() == 0.0 && (0.0..=255.0).contains(number))
84                .map(|number| {
85                    #[allow(
86                        clippy::cast_possible_truncation,
87                        clippy::cast_sign_loss,
88                        reason = "the filter has just checked it is a whole number in 0..=255"
89                    )]
90                    let byte = number as u8;
91                    byte
92                })
93                .ok_or_else(|| Error::Decode("a BLOB element came back that is not a byte".into()))
94        })
95        .collect()
96}
97
98/// A [`DatabaseError`] for what the binding threw.
99///
100/// The thrown error's `message` is the backend's summary, and its `cause`,
101/// when there is one, carries SQLite's own text -- the part the `ErrorKind`
102/// mapping reads -- so both are kept.
103#[must_use]
104pub fn database_error(error: &JsValue) -> Error {
105    let message = match error.dyn_ref::<js_sys::Error>() {
106        Some(error) => {
107            let message = String::from(error.message());
108
109            match error.cause().dyn_ref::<js_sys::Error>() {
110                Some(cause) => {
111                    let cause = String::from(cause.message());
112                    if message.contains(&cause) {
113                        message
114                    } else {
115                        format!("{message}: {cause}")
116                    }
117                }
118                None => message,
119            }
120        }
121        None => error.as_string().unwrap_or_else(|| format!("{error:?}")),
122    };
123
124    Error::Database(Box::new(DatabaseError::new(message)))
125}