| 64 | 64 | | } |
| 65 | 65 | | } |
| 66 | 66 | | |
| 67 | + | /// A value to bind, or SQL NULL. `Option::into` gives JavaScript |
| 68 | + | /// `undefined` for None, which D1 refuses ("Type 'undefined' not |
| 69 | + | /// supported"): a customer without a card, or a replay that finished. |
| 70 | + | fn or_null<T: Into<worker::wasm_bindgen::JsValue>>(value: Option<T>) -> worker::wasm_bindgen::JsValue { |
| 71 | + | value.map_or(worker::wasm_bindgen::JsValue::NULL, Into::into) |
| 72 | + | } |
| 73 | + | |
| 67 | 74 | | impl Billing { |
| 68 | 75 | | /// The workspace's saved card as last synced. Stripe is asked only when |
| 69 | 76 | | /// it never was, or g1t changed the card since; a Stripe that does not |
| ⋯ |
| 130 | 137 | | ) |
| 131 | 138 | | .bind(&[ |
| 132 | 139 | | customer.into(), |
| 133 | | − | card.as_ref().map(|c| c.brand.clone()).into(), |
| 134 | | − | card.as_ref().map(|c| c.last4.clone()).into(), |
| 135 | | − | card.as_ref().map(|c| c.exp_month as f64).into(), |
| 136 | | − | card.as_ref().map(|c| c.exp_year as f64).into(), |
| 140 | + | or_null(card.as_ref().map(|c| c.brand.clone())), |
| 141 | + | or_null(card.as_ref().map(|c| c.last4.clone())), |
| 142 | + | or_null(card.as_ref().map(|c| c.exp_month as f64)), |
| 143 | + | or_null(card.as_ref().map(|c| c.exp_year as f64)), |
| 137 | 144 | | rfc3339(now_ms()).into(), |
| 138 | 145 | | ])? |
| 139 | 146 | | .run() |
| ⋯ |
| 249 | 256 | | .bind(&[ |
| 250 | 257 | | mode.into(), (through as f64).into(), rfc3339(now_ms()).into(), |
| 251 | 258 | | crate::optional(if complete { None } else { after.as_deref() }), |
| 252 | | − | (!complete).then_some(since as f64).into(), |
| 253 | | − | (!complete).then_some(until as f64).into(), |
| 254 | | − | if complete { None } else { first_failed.map(|v| v as f64) }.into(), |
| 259 | + | or_null((!complete).then_some(since as f64)), |
| 260 | + | or_null((!complete).then_some(until as f64)), |
| 261 | + | or_null(if complete { None } else { first_failed.map(|v| v as f64) }), |
| 255 | 262 | | ])? |
| 256 | 263 | | .run() |
| 257 | 264 | | .await?; |
| ⋯ |
| 354 | 361 | | |
| 355 | 362 | | #[cfg(test)] |
| 356 | 363 | | mod tests { |
| 364 | + | |
| 365 | + | #[test] |
| 366 | + | fn no_binding_turns_none_into_undefined() { |
| 367 | + | // D1 refuses `undefined`; every optional value goes through or_null. |
| 368 | + | let source = include_str!("stripe_sync.rs"); |
| 369 | + | let body = source.split("#[cfg(test)]").next().unwrap(); |
| 370 | + | for line in body.lines().filter(|l| l.trim_end().ends_with(".into(),")) { |
| 371 | + | assert!(!line.contains("then_some(") && !line.contains(".map(|"), "{line}"); |
| 372 | + | } |
| 373 | + | } |
| 374 | + | |
| 357 | 375 | | use super::*; |
| 358 | 376 | | |
| 359 | 377 | | #[test] |