1use 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 #[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}