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 [`perspective_client::Schema`], notifying any
249 /// derived [`View`] and [`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}