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 js_sys::Function;
14use perspective_client::config::*;
15use perspective_client::{DeleteOptions, UpdateData, UpdateOptions, assert_table_api};
16use wasm_bindgen::prelude::*;
17use wasm_bindgen_derive::TryFromJsValue;
18use wasm_bindgen_futures::spawn_local;
19
20use crate::Client;
21use crate::table_data::UpdateDataExt;
22use crate::utils::{ApiFuture, ApiResult, JsValueSerdeExt, LocalPollLoop};
23pub use crate::view::*;
24
25#[derive(TryFromJsValue, Clone, PartialEq)]
26#[wasm_bindgen]
27pub struct Table(pub(crate) perspective_client::Table);
28
29assert_table_api!(Table);
30
31impl From<perspective_client::Table> for Table {
32    fn from(value: perspective_client::Table) -> Self {
33        Table(value)
34    }
35}
36
37impl Table {
38    pub fn get_table(&self) -> &'_ perspective_client::Table {
39        &self.0
40    }
41}
42
43#[wasm_bindgen]
44extern "C" {
45    // TODO Fix me
46    #[wasm_bindgen(typescript_type = "\
47        string | ArrayBuffer | Record<string, unknown[]> | Record<string, unknown>[]")]
48    pub type JsTableInitData;
49
50    #[wasm_bindgen(typescript_type = "ViewConfigUpdate")]
51    pub type JsViewConfig;
52
53    #[wasm_bindgen(typescript_type = "UpdateOptions")]
54    pub type JsUpdateOptions;
55
56    #[wasm_bindgen(typescript_type = "DeleteOptions")]
57    pub type JsDeleteOptions;
58}
59
60#[wasm_bindgen]
61impl Table {
62    /// Returns the name of the index column for the table.
63    ///
64    /// # JavaScript Examples
65    ///
66    /// ```javascript
67    /// const table = await client.table("x,y\n1,2\n3,4", { index: "x" });
68    /// const index = table.get_index(); // "x"
69    /// ```
70    #[wasm_bindgen]
71    pub async fn get_index(&self) -> Option<String> {
72        self.0.get_index()
73    }
74
75    /// Get a copy of the [`Client`] this [`Table`] came from.
76    #[wasm_bindgen]
77    pub async fn get_client(&self) -> Client {
78        Client {
79            close: None,
80            client: self.0.get_client(),
81        }
82    }
83
84    /// Returns the user-specified name for this table, or the auto-generated
85    /// name if a name was not specified when the table was created.
86    #[wasm_bindgen]
87    pub async fn get_name(&self) -> String {
88        self.0.get_name().to_owned()
89    }
90
91    /// Returns the user-specified row limit for this table.
92    #[wasm_bindgen]
93    pub async fn get_limit(&self) -> Option<u32> {
94        self.0.get_limit()
95    }
96
97    /// Removes all the rows in the [`Table`], but preserves everything else
98    /// including the schema, index, and any callbacks or registered
99    /// [`View`] instances.
100    ///
101    /// Calling [`Table::clear`], like [`Table::update`] and [`Table::remove`],
102    /// will trigger an update event to any registered listeners via
103    /// [`View::on_update`].
104    #[wasm_bindgen]
105    pub async fn clear(&self) -> ApiResult<()> {
106        self.0.clear().await?;
107        Ok(())
108    }
109
110    /// Delete this [`Table`] and cleans up associated resources.
111    ///
112    /// [`Table`]s do not stop consuming resources or processing updates when
113    /// they are garbage collected in their host language - you must call
114    /// this method to reclaim these.
115    ///
116    /// # Arguments
117    ///
118    /// - `options` An options dictionary.
119    ///     - `lazy` Whether to delete this [`Table`] _lazily_. When false (the
120    ///       default), the delete will occur immediately, assuming it has no
121    ///       [`View`] instances registered to it (which must be deleted first,
122    ///       otherwise this method will throw an error). When true, the
123    ///       [`Table`] will only be marked for deltion once its [`View`]
124    ///       dependency count reaches 0.
125    ///
126    /// # JavaScript Examples
127    ///
128    /// ```javascript
129    /// const table = await client.table("x,y\n1,2\n3,4");
130    ///
131    /// // ...
132    ///
133    /// await table.delete({ lazy: true });
134    /// ```
135    #[wasm_bindgen]
136    pub async fn delete(self, options: Option<JsDeleteOptions>) -> ApiResult<()> {
137        let options = options
138            .into_serde_ext::<Option<DeleteOptions>>()?
139            .unwrap_or_default();
140
141        self.0.delete(options).await?;
142        Ok(())
143    }
144
145    /// Returns the number of rows in a [`Table`].
146    #[wasm_bindgen]
147    pub async fn size(&self) -> ApiResult<f64> {
148        Ok(self.0.size().await? as f64)
149    }
150
151    /// Returns a table's [`Schema`], a mapping of column names to column types.
152    ///
153    /// The mapping of a [`Table`]'s column names to data types is referred to
154    /// as a [`Schema`]. Each column has a unique name and a data type, one
155    /// of:
156    ///
157    /// - `"boolean"` - A boolean type
158    /// - `"date"` - A timesonze-agnostic date type (month/day/year)
159    /// - `"datetime"` - A millisecond-precision datetime type in the UTC
160    ///   timezone
161    /// - `"float"` - A 64 bit float
162    /// - `"integer"` - A signed 32 bit integer (the integer type supported by
163    ///   JavaScript)
164    /// - `"string"` - A [`String`] data type (encoded internally as a
165    ///   _dictionary_)
166    ///
167    /// Note that all [`Table`] columns are _nullable_, regardless of the data
168    /// type.
169    #[wasm_bindgen]
170    pub async fn schema(&self) -> ApiResult<JsValue> {
171        let schema = self.0.schema().await?;
172        Ok(JsValue::from_serde_ext(&schema)?)
173    }
174
175    /// Returns the column names of this [`Table`] in "natural" order (the
176    /// ordering implied by the input format).
177    ///  
178    ///  # JavaScript Examples
179    ///
180    ///  ```javascript
181    ///  const columns = await table.columns();
182    ///  ```   
183    #[wasm_bindgen]
184    pub async fn columns(&self) -> ApiResult<JsValue> {
185        let columns = self.0.columns().await?;
186        Ok(JsValue::from_serde_ext(&columns)?)
187    }
188
189    /// Create a unique channel ID on this [`Table`], which allows
190    /// `View::on_update` callback calls to be associated with the
191    /// `Table::update` which caused them.
192    #[wasm_bindgen]
193    pub async fn make_port(&self) -> ApiResult<i32> {
194        Ok(self.0.make_port().await?)
195    }
196
197    /// Register a callback which is called exactly once, when this [`Table`] is
198    /// deleted with the [`Table::delete`] method.
199    ///
200    /// [`Table::on_delete`] resolves when the subscription message is sent, not
201    /// when the _delete_ event occurs.
202    #[wasm_bindgen]
203    pub fn on_delete(&self, on_delete: Function) -> ApiFuture<u32> {
204        let table = self.clone();
205        ApiFuture::new(async move {
206            let emit = LocalPollLoop::new(move |()| on_delete.call0(&JsValue::UNDEFINED));
207            let on_delete = Box::new(move || spawn_local(emit.poll(())));
208            Ok(table.0.on_delete(on_delete).await?)
209        })
210    }
211
212    /// Removes a listener with a given ID, as returned by a previous call to
213    /// [`Table::on_delete`].
214    #[wasm_bindgen]
215    pub fn remove_delete(&self, callback_id: u32) -> ApiFuture<()> {
216        let client = self.0.clone();
217        ApiFuture::new(async move {
218            client.remove_delete(callback_id).await?;
219            Ok(())
220        })
221    }
222
223    /// Removes rows from this [`Table`] with the `index` column values
224    /// supplied.
225    ///
226    /// # Arguments
227    ///
228    /// - `indices` - A list of `index` column values for rows that should be
229    ///   removed.
230    ///
231    /// # JavaScript Examples
232    ///
233    /// ```javascript
234    /// await table.remove([1, 2, 3]);
235    /// ```
236    #[wasm_bindgen]
237    pub async fn remove(&self, value: &JsValue, options: Option<JsUpdateOptions>) -> ApiResult<()> {
238        let options = options
239            .into_serde_ext::<Option<UpdateOptions>>()?
240            .unwrap_or_default();
241
242        let input = UpdateData::from_js_value(value, options.format)?;
243        self.0.remove(input).await?;
244        Ok(())
245    }
246
247    /// Replace all rows in this [`Table`] with the input data, coerced to this
248    /// [`Table`]'s existing [`Schema`], notifying any derived [`View`] and
249    /// [`View::on_update`] callbacks.
250    ///
251    /// Calling [`Table::replace`] is an easy way to replace _all_ the data in a
252    /// [`Table`] without losing any derived [`View`] instances or
253    /// [`View::on_update`] callbacks. [`Table::replace`] does _not_ infer
254    /// data types like [`Client::table`] does, rather it _coerces_ input
255    /// data to the `Schema` like [`Table::update`]. If you need a [`Table`]
256    /// with a different `Schema`, you must create a new one.
257    ///
258    /// # JavaScript Examples
259    ///
260    /// ```javascript
261    /// await table.replace("x,y\n1,2");
262    /// ```
263    #[wasm_bindgen]
264    pub async fn replace(
265        &self,
266        input: &JsValue,
267        options: Option<JsUpdateOptions>,
268    ) -> ApiResult<()> {
269        let options = options
270            .into_serde_ext::<Option<UpdateOptions>>()?
271            .unwrap_or_default();
272
273        let input = UpdateData::from_js_value(input, options.format)?;
274        self.0.replace(input).await?;
275        Ok(())
276    }
277
278    /// Updates the rows of this table and any derived [`View`] instances.
279    ///
280    /// Calling [`Table::update`] will trigger the [`View::on_update`] callbacks
281    /// register to derived [`View`], and the call itself will not resolve until
282    /// _all_ derived [`View`]'s are notified.
283    ///
284    /// When updating a [`Table`] with an `index`, [`Table::update`] supports
285    /// partial updates, by omitting columns from the update data.
286    ///
287    /// # Arguments
288    ///
289    /// - `input` - The input data for this [`Table`]. The schema of a [`Table`]
290    ///   is immutable after creation, so this method cannot be called with a
291    ///   schema.
292    /// - `options` - Options for this update step - see [`UpdateOptions`].
293    ///
294    /// # JavaScript Examples
295    ///
296    /// ```javascript
297    /// await table.update("x,y\n1,2");
298    /// ```
299    #[wasm_bindgen]
300    pub fn update(
301        &self,
302        input: JsTableInitData,
303        options: Option<JsUpdateOptions>,
304    ) -> ApiFuture<()> {
305        let table = self.clone();
306        ApiFuture::new(async move {
307            let options = options
308                .into_serde_ext::<Option<UpdateOptions>>()?
309                .unwrap_or_default();
310
311            let input = UpdateData::from_js_value(&input, options.format)?;
312            Ok(table.0.update(input, options).await?)
313        })
314    }
315
316    /// Create a new [`View`] from this table with a specified
317    /// [`ViewConfigUpdate`].
318    ///
319    /// See [`View`] struct.
320    ///
321    /// # JavaScript Examples
322    ///
323    /// ```javascript
324    /// const view = await table.view({
325    ///     columns: ["Sales"],
326    ///     aggregates: { Sales: "sum" },
327    ///     group_by: ["Region", "Country"],
328    ///     filter: [["Category", "in", ["Furniture", "Technology"]]],
329    /// });
330    /// ```
331    #[wasm_bindgen]
332    pub async fn view(&self, config: Option<JsViewConfig>) -> ApiResult<View> {
333        let config = config
334            .map(|config| js_sys::JSON::stringify(&config))
335            .transpose()?
336            .and_then(|x| x.as_string())
337            .map(|x| serde_json::from_str(x.as_str()))
338            .transpose()?;
339
340        let view = self.0.view(config).await?;
341        Ok(View(view))
342    }
343
344    /// Validates the given expressions.
345    #[wasm_bindgen]
346    pub async fn validate_expressions(&self, exprs: &JsValue) -> ApiResult<JsValue> {
347        let exprs = JsValue::into_serde_ext::<Expressions>(exprs.clone())?;
348        let columns = self.0.validate_expressions(exprs).await?;
349        Ok(JsValue::from_serde_ext(&columns)?)
350    }
351}