perspective_js/
table.rs

1// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
3// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
4// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
5// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
6// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7// ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
8// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9// ┃ This file is part of the Perspective library, distributed under the terms ┃
10// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
13use extend::ext;
14use js_sys::{Array, ArrayBuffer, Function, JSON, Object, Reflect, Uint8Array};
15use macro_rules_attribute::apply;
16use perspective_client::config::*;
17use perspective_client::{
18    ColumnType, TableData, TableReadFormat, UpdateData, UpdateOptions, assert_table_api,
19};
20use wasm_bindgen::convert::TryFromJsValue;
21use wasm_bindgen::prelude::*;
22use wasm_bindgen_derive::TryFromJsValue;
23use wasm_bindgen_futures::spawn_local;
24
25use crate::client::Client;
26use crate::utils::{
27    ApiError, ApiFuture, ApiResult, JsValueSerdeExt, LocalPollLoop, ToApiError, inherit_docs,
28};
29pub use crate::view::*;
30
31#[ext]
32impl Vec<(String, ColumnType)> {
33    fn from_js_value(value: &JsValue) -> ApiResult<Vec<(String, ColumnType)>> {
34        Ok(Object::keys(value.unchecked_ref())
35            .iter()
36            .map(|x| -> Result<_, JsValue> {
37                let key = x.as_string().into_apierror()?;
38                let val = Reflect::get(value, &x)?
39                    .as_string()
40                    .into_apierror()?
41                    .into_serde_ext()?;
42
43                Ok((key, val))
44            })
45            .collect::<Result<Vec<_>, _>>()?)
46    }
47}
48
49#[ext]
50pub(crate) impl TableData {
51    fn from_js_value(value: &JsValue, format: Option<TableReadFormat>) -> ApiResult<TableData> {
52        let err_fn = || JsValue::from(format!("Failed to construct Table {:?}", value));
53        if let Some(result) = UpdateData::from_js_value_partial(value, format)? {
54            Ok(result.into())
55        } else if value.is_instance_of::<Object>() && Reflect::has(value, &"__get_model".into())? {
56            let val = Reflect::get(value, &"__get_model".into())?
57                .dyn_into::<Function>()?
58                .call0(value)?;
59
60            let view = View::try_from_js_value(val)?;
61            Ok(TableData::View(view.0))
62        } else if value.is_instance_of::<Object>() {
63            let all_strings = || {
64                Object::values(value.unchecked_ref())
65                    .to_vec()
66                    .iter()
67                    .all(|x| x.is_string())
68            };
69
70            let all_arrays = || {
71                Object::values(value.unchecked_ref())
72                    .to_vec()
73                    .iter()
74                    .all(|x| x.is_instance_of::<Array>())
75            };
76
77            if all_strings() {
78                Ok(TableData::Schema(Vec::from_js_value(value)?))
79            } else if all_arrays() {
80                let json = JSON::stringify(value)?.as_string().into_apierror()?;
81                Ok(UpdateData::JsonColumns(json).into())
82            } else {
83                Err(err_fn().into())
84            }
85        } else {
86            Err(err_fn().into())
87        }
88    }
89}
90
91#[ext]
92pub(crate) impl UpdateData {
93    fn from_js_value_partial(
94        value: &JsValue,
95        format: Option<TableReadFormat>,
96    ) -> ApiResult<Option<UpdateData>> {
97        let err_fn = || JsValue::from(format!("Failed to construct Table {:?}", value));
98        if value.is_undefined() {
99            Err(err_fn().into())
100        } else if value.is_string() {
101            match format {
102                None | Some(TableReadFormat::Csv) => {
103                    Ok(Some(UpdateData::Csv(value.as_string().into_apierror()?)))
104                },
105                Some(TableReadFormat::JsonString) => Ok(Some(UpdateData::JsonRows(
106                    value.as_string().into_apierror()?,
107                ))),
108                Some(TableReadFormat::ColumnsString) => Ok(Some(UpdateData::JsonColumns(
109                    value.as_string().into_apierror()?,
110                ))),
111                Some(TableReadFormat::Arrow) => Ok(Some(UpdateData::Arrow(
112                    value.as_string().into_apierror()?.into_bytes().into(),
113                ))),
114                Some(TableReadFormat::Ndjson) => {
115                    Ok(Some(UpdateData::Ndjson(value.as_string().into_apierror()?)))
116                },
117            }
118        } else if value.is_instance_of::<ArrayBuffer>() {
119            let uint8array = Uint8Array::new(value);
120            let slice = uint8array.to_vec();
121            match format {
122                Some(TableReadFormat::Csv) => Ok(Some(UpdateData::Csv(String::from_utf8(slice)?))),
123                Some(TableReadFormat::JsonString) => {
124                    Ok(Some(UpdateData::JsonRows(String::from_utf8(slice)?)))
125                },
126                Some(TableReadFormat::ColumnsString) => {
127                    Ok(Some(UpdateData::JsonColumns(String::from_utf8(slice)?)))
128                },
129                Some(TableReadFormat::Ndjson) => {
130                    Ok(Some(UpdateData::Ndjson(String::from_utf8(slice)?)))
131                },
132                None | Some(TableReadFormat::Arrow) => Ok(Some(UpdateData::Arrow(slice.into()))),
133            }
134        } else if let Some(uint8array) = value.dyn_ref::<Uint8Array>() {
135            let slice = uint8array.to_vec();
136            match format {
137                Some(TableReadFormat::Csv) => Ok(Some(UpdateData::Csv(String::from_utf8(slice)?))),
138                Some(TableReadFormat::JsonString) => {
139                    Ok(Some(UpdateData::JsonRows(String::from_utf8(slice)?)))
140                },
141                Some(TableReadFormat::ColumnsString) => {
142                    Ok(Some(UpdateData::JsonColumns(String::from_utf8(slice)?)))
143                },
144                Some(TableReadFormat::Ndjson) => {
145                    Ok(Some(UpdateData::Ndjson(String::from_utf8(slice)?)))
146                },
147                None | Some(TableReadFormat::Arrow) => Ok(Some(UpdateData::Arrow(slice.into()))),
148            }
149        } else if value.is_instance_of::<Array>() {
150            let rows = JSON::stringify(value)?.as_string().into_apierror()?;
151            Ok(Some(UpdateData::JsonRows(rows)))
152        } else {
153            Ok(None)
154        }
155    }
156
157    fn from_js_value(value: &JsValue, format: Option<TableReadFormat>) -> ApiResult<UpdateData> {
158        match TableData::from_js_value(value, format)? {
159            TableData::Schema(_) => Err(ApiError::new(
160                "Method cannot be called with `Schema` argument",
161            )),
162            TableData::Update(x) => Ok(x),
163            TableData::View(_) => Err(ApiError::new(
164                "Method cannot be called with `Schema` argument",
165            )),
166        }
167    }
168}
169
170#[derive(TryFromJsValue, Clone)]
171#[wasm_bindgen]
172pub struct Table(pub(crate) perspective_client::Table);
173
174assert_table_api!(Table);
175
176impl From<perspective_client::Table> for Table {
177    fn from(value: perspective_client::Table) -> Self {
178        Table(value)
179    }
180}
181
182impl Table {
183    pub fn get_table(&self) -> &'_ perspective_client::Table {
184        &self.0
185    }
186}
187
188#[wasm_bindgen]
189extern "C" {
190    // TODO Fix me
191    #[wasm_bindgen(typescript_type = "\
192        string | ArrayBuffer | Record<string, unknown[]> | Record<string, unknown>[]")]
193    pub type JsTableInitData;
194
195    #[wasm_bindgen(typescript_type = "ViewConfigUpdate")]
196    pub type JsViewConfig;
197
198    #[wasm_bindgen(typescript_type = "UpdateOptions")]
199    pub type JsUpdateOptions;
200}
201
202#[wasm_bindgen]
203impl Table {
204    #[apply(inherit_docs)]
205    #[inherit_doc = "table/get_index.md"]
206    #[wasm_bindgen]
207    pub async fn get_index(&self) -> Option<String> {
208        self.0.get_index()
209    }
210
211    #[apply(inherit_docs)]
212    #[inherit_doc = "table/get_client.md"]
213    #[wasm_bindgen]
214    pub async fn get_client(&self) -> Client {
215        Client {
216            close: None,
217            client: self.0.get_client(),
218        }
219    }
220
221    #[apply(inherit_docs)]
222    #[inherit_doc = "table/get_name.md"]
223    #[wasm_bindgen]
224    pub async fn get_name(&self) -> String {
225        self.0.get_name().to_owned()
226    }
227
228    #[apply(inherit_docs)]
229    #[inherit_doc = "table/get_limit.md"]
230    #[wasm_bindgen]
231    pub async fn get_limit(&self) -> Option<u32> {
232        self.0.get_limit()
233    }
234
235    #[apply(inherit_docs)]
236    #[inherit_doc = "table/clear.md"]
237    #[wasm_bindgen]
238    pub async fn clear(&self) -> ApiResult<()> {
239        self.0.clear().await?;
240        Ok(())
241    }
242
243    #[apply(inherit_docs)]
244    #[inherit_doc = "table/delete.md"]
245    #[wasm_bindgen]
246    pub async fn delete(&self) -> ApiResult<()> {
247        self.0.delete().await?;
248        Ok(())
249    }
250
251    #[apply(inherit_docs)]
252    #[inherit_doc = "table/size.md"]
253    #[wasm_bindgen]
254    pub async fn size(&self) -> ApiResult<f64> {
255        Ok(self.0.size().await? as f64)
256    }
257
258    #[apply(inherit_docs)]
259    #[inherit_doc = "table/schema.md"]
260    #[wasm_bindgen]
261    pub async fn schema(&self) -> ApiResult<JsValue> {
262        let schema = self.0.schema().await?;
263        Ok(JsValue::from_serde_ext(&schema)?)
264    }
265
266    #[apply(inherit_docs)]
267    #[inherit_doc = "table/columns.md"]
268    #[wasm_bindgen]
269    pub async fn columns(&self) -> ApiResult<JsValue> {
270        let columns = self.0.columns().await?;
271        Ok(JsValue::from_serde_ext(&columns)?)
272    }
273
274    #[apply(inherit_docs)]
275    #[inherit_doc = "table/make_port.md"]
276    #[wasm_bindgen]
277    pub async fn make_port(&self) -> ApiResult<i32> {
278        Ok(self.0.make_port().await?)
279    }
280
281    #[apply(inherit_docs)]
282    #[inherit_doc = "table/on_delete.md"]
283    #[wasm_bindgen]
284    pub async fn on_delete(&self, on_delete: Function) -> ApiResult<u32> {
285        let emit = LocalPollLoop::new(move |()| on_delete.call0(&JsValue::UNDEFINED));
286        let on_delete = Box::new(move || spawn_local(emit.poll(())));
287        Ok(self.0.on_delete(on_delete).await?)
288    }
289
290    #[apply(inherit_docs)]
291    #[inherit_doc = "table/remove_delete.md"]
292    #[wasm_bindgen]
293    pub fn remove_delete(&self, callback_id: u32) -> ApiFuture<()> {
294        let client = self.0.clone();
295        ApiFuture::new(async move {
296            client.remove_delete(callback_id).await?;
297            Ok(())
298        })
299    }
300
301    #[apply(inherit_docs)]
302    #[inherit_doc = "table/replace.md"]
303    #[wasm_bindgen]
304    pub async fn remove(&self, value: &JsValue, options: Option<JsUpdateOptions>) -> ApiResult<()> {
305        let options = options
306            .into_serde_ext::<Option<UpdateOptions>>()?
307            .unwrap_or_default();
308
309        let input = UpdateData::from_js_value(value, options.format)?;
310        self.0.remove(input).await?;
311        Ok(())
312    }
313
314    #[apply(inherit_docs)]
315    #[inherit_doc = "table/replace.md"]
316    #[wasm_bindgen]
317    pub async fn replace(
318        &self,
319        input: &JsValue,
320        options: Option<JsUpdateOptions>,
321    ) -> ApiResult<()> {
322        let options = options
323            .into_serde_ext::<Option<UpdateOptions>>()?
324            .unwrap_or_default();
325
326        let input = UpdateData::from_js_value(input, options.format)?;
327        self.0.replace(input).await?;
328        Ok(())
329    }
330
331    #[apply(inherit_docs)]
332    #[inherit_doc = "table/update.md"]
333    #[wasm_bindgen]
334    pub async fn update(
335        &self,
336        input: &JsTableInitData,
337        options: Option<JsUpdateOptions>,
338    ) -> ApiResult<()> {
339        let options = options
340            .into_serde_ext::<Option<UpdateOptions>>()?
341            .unwrap_or_default();
342
343        let input = UpdateData::from_js_value(input, options.format)?;
344        self.0.update(input, options).await?;
345        Ok(())
346    }
347
348    #[apply(inherit_docs)]
349    #[inherit_doc = "table/view.md"]
350    #[wasm_bindgen]
351    pub async fn view(&self, config: Option<JsViewConfig>) -> ApiResult<View> {
352        let config = config
353            .map(|config| js_sys::JSON::stringify(&config))
354            .transpose()?
355            .and_then(|x| x.as_string())
356            .map(|x| serde_json::from_str(x.as_str()))
357            .transpose()?;
358
359        let view = self.0.view(config).await?;
360        Ok(View(view))
361    }
362
363    #[apply(inherit_docs)]
364    #[inherit_doc = "table/validate_expressions.md"]
365    #[wasm_bindgen]
366    pub async fn validate_expressions(&self, exprs: &JsValue) -> ApiResult<JsValue> {
367        let exprs = JsValue::into_serde_ext::<Expressions>(exprs.clone())?;
368        let columns = self.0.validate_expressions(exprs).await?;
369        Ok(JsValue::from_serde_ext(&columns)?)
370    }
371}