| 117 | 117 | | /// Newest first. Callers must have checked that the viewer may see the |
| 118 | 118 | | /// repository asked about. |
| 119 | 119 | | async fn list(&self, a: ListArgs) -> Result<Vec<Event>> { |
| 120 | | − | let mut conditions = Vec::new(); |
| 121 | | − | let mut values: Vec<JsValue> = Vec::new(); |
| 122 | | − | if let Some(repo_id) = &a.repo_id { |
| 123 | | − | conditions.push("repo_id = ?".to_owned()); |
| 124 | | − | values.push(repo_id.as_str().into()); |
| 125 | | − | } |
| 126 | | − | if !a.types.is_empty() { |
| 127 | | − | let marks = vec!["?"; a.types.len()].join(", "); |
| 128 | | − | conditions.push(format!("type IN ({marks})")); |
| 129 | | − | values.extend(a.types.iter().map(|kind| JsValue::from(kind.as_str()))); |
| 130 | | − | } else { |
| 131 | | − | // What CI and integrations report on commits goes to webhooks, |
| 132 | | − | // and is read from each commit's checks; a timeline asked for |
| 133 | | − | // everything would be little else on a busy repository. |
| 134 | | − | let marks = vec!["?"; REPORTING.len()].join(", "); |
| 135 | | − | conditions.push(format!("type NOT IN ({marks})")); |
| 136 | | − | values.extend(REPORTING.iter().map(|kind| JsValue::from(*kind))); |
| 137 | | − | } |
| 138 | | − | if let Some(before) = &a.before { |
| 139 | | − | conditions.push("id < ?".to_owned()); |
| 140 | | − | values.push(before.as_str().into()); |
| 141 | | − | } |
| 142 | | − | let filter = if conditions.is_empty() { |
| 143 | | − | String::new() |
| 144 | | − | } else { |
| 145 | | − | format!("WHERE {}", conditions.join(" AND ")) |
| 146 | | − | }; |
| 120 | + | let (filter, binds) = list_filter(&a); |
| 121 | + | let mut values: Vec<JsValue> = binds |
| 122 | + | .into_iter() |
| 123 | + | .map(|bind| match bind { |
| 124 | + | Bind::Text(text) => JsValue::from(text), |
| 125 | + | Bind::Number(number) => JsValue::from(number), |
| 126 | + | }) |
| 127 | + | .collect(); |
| 147 | 128 | | values.push(a.limit.unwrap_or(DEFAULT_PAGE).min(MAX_PAGE).into()); |
| 148 | 129 | | let rows = self |
| 149 | 130 | | .db |
| ⋯ |
| 319 | 300 | | Err(error) => worker::console_error!("audit entries kept past their plan's days: {error}"), |
| 320 | 301 | | } |
| 321 | 302 | | } |
| 303 | + | |
| 304 | + | /// A value bound to a `?` in [`list_filter`]'s clause. |
| 305 | + | #[derive(Debug, PartialEq)] |
| 306 | + | enum Bind { |
| 307 | + | Text(String), |
| 308 | + | Number(f64), |
| 309 | + | } |
| 310 | + | |
| 311 | + | /// The `WHERE` clause `list` reads with, and what it binds, in order. |
| 312 | + | fn list_filter(a: &ListArgs) -> (String, Vec<Bind>) { |
| 313 | + | let mut conditions = Vec::new(); |
| 314 | + | let mut values = Vec::new(); |
| 315 | + | if let Some(repo_id) = &a.repo_id { |
| 316 | + | conditions.push("repo_id = ?".to_owned()); |
| 317 | + | values.push(Bind::Text(repo_id.clone())); |
| 318 | + | } |
| 319 | + | if !a.types.is_empty() { |
| 320 | + | let marks = vec!["?"; a.types.len()].join(", "); |
| 321 | + | conditions.push(format!("type IN ({marks})")); |
| 322 | + | values.extend(a.types.iter().map(|kind| Bind::Text(kind.clone()))); |
| 323 | + | } else { |
| 324 | + | // What CI and integrations report on commits goes to webhooks, |
| 325 | + | // and is read from each commit's checks; a timeline asked for |
| 326 | + | // everything would be little else on a busy repository. |
| 327 | + | let marks = vec!["?"; REPORTING.len()].join(", "); |
| 328 | + | conditions.push(format!("type NOT IN ({marks})")); |
| 329 | + | values.extend(REPORTING.iter().map(|kind| Bind::Text((*kind).to_owned()))); |
| 330 | + | } |
| 331 | + | if let Some(actor) = &a.actor { |
| 332 | + | conditions.push("actor = ?".to_owned()); |
| 333 | + | values.push(Bind::Text(actor.clone())); |
| 334 | + | } |
| 335 | + | if !a.numbers.is_empty() { |
| 336 | + | // An issue or pull request's own events name it as `number`; a |
| 337 | + | // comment, review or link on it names it as `issue`. |
| 338 | + | let marks = vec!["?"; a.numbers.len()].join(", "); |
| 339 | + | conditions.push(format!( |
| 340 | + | "(json_extract(data, '$.number') IN ({marks}) OR json_extract(data, '$.issue') IN ({marks}))" |
| 341 | + | )); |
| 342 | + | for _ in 0..2 { |
| 343 | + | values.extend(a.numbers.iter().map(|number| Bind::Number(f64::from(*number)))); |
| 344 | + | } |
| 345 | + | } |
| 346 | + | if let Some(since) = &a.since { |
| 347 | + | conditions.push("time >= ?".to_owned()); |
| 348 | + | values.push(Bind::Text(since.clone())); |
| 349 | + | } |
| 350 | + | if let Some(before) = &a.before { |
| 351 | + | conditions.push("id < ?".to_owned()); |
| 352 | + | values.push(Bind::Text(before.clone())); |
| 353 | + | } |
| 354 | + | let filter = if conditions.is_empty() { |
| 355 | + | String::new() |
| 356 | + | } else { |
| 357 | + | format!("WHERE {}", conditions.join(" AND ")) |
| 358 | + | }; |
| 359 | + | (filter, values) |
| 360 | + | } |
| 361 | + | |
| 362 | + | #[cfg(test)] |
| 363 | + | mod tests { |
| 364 | + | use super::*; |
| 365 | + | |
| 366 | + | #[test] |
| 367 | + | fn a_plain_list_leaves_out_what_is_reported_on_commits() { |
| 368 | + | let (filter, binds) = list_filter(&ListArgs { repo_id: Some("rep_1".into()), ..ListArgs::default() }); |
| 369 | + | assert!(filter.starts_with("WHERE repo_id = ? AND type NOT IN (")); |
| 370 | + | assert_eq!(binds[0], Bind::Text("rep_1".into())); |
| 371 | + | assert_eq!(binds.len(), 1 + REPORTING.len()); |
| 372 | + | } |
| 373 | + | |
| 374 | + | #[test] |
| 375 | + | fn numbers_match_an_item_or_what_is_said_on_it_since_a_time() { |
| 376 | + | let (filter, binds) = list_filter(&ListArgs { |
| 377 | + | repo_id: Some("rep_1".into()), |
| 378 | + | types: vec!["issue.opened".into()], |
| 379 | + | numbers: vec![4, 9], |
| 380 | + | since: Some("2026-10-01T00:00:00Z".into()), |
| 381 | + | actor: Some("usr_g1t_agent".into()), |
| 382 | + | ..ListArgs::default() |
| 383 | + | }); |
| 384 | + | assert_eq!( |
| 385 | + | filter, |
| 386 | + | "WHERE repo_id = ? AND type IN (?) AND actor = ? AND \ |
| 387 | + | (json_extract(data, '$.number') IN (?, ?) OR json_extract(data, '$.issue') IN (?, ?)) AND time >= ?" |
| 388 | + | ); |
| 389 | + | assert_eq!( |
| 390 | + | binds, |
| 391 | + | vec![ |
| 392 | + | Bind::Text("rep_1".into()), |
| 393 | + | Bind::Text("issue.opened".into()), |
| 394 | + | Bind::Text("usr_g1t_agent".into()), |
| 395 | + | Bind::Number(4.0), |
| 396 | + | Bind::Number(9.0), |
| 397 | + | Bind::Number(4.0), |
| 398 | + | Bind::Number(9.0), |
| 399 | + | Bind::Text("2026-10-01T00:00:00Z".into()), |
| 400 | + | ] |
| 401 | + | ); |
| 402 | + | } |
| 403 | + | } |