use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::fs;
use super::utils::JiraContext;
use crate::query::UrlParamsBuilder;
pub async fn list_audit_records(
ctx: &JiraContext<'_>,
from: Option<&str>,
to: Option<&str>,
filter: Option<&str>,
limit: Option<usize>,
) -> Result<()> {
#[derive(Deserialize)]
struct AuditResponse {
records: Vec<AuditRecord>,
}
#[derive(Deserialize)]
struct AuditRecord {
id: i64,
summary: String,
#[serde(rename = "objectItem")]
object_item: ObjectItem,
#[serde(rename = "authorKey")]
author_key: Option<String>,
created: String,
category: String,
}
#[derive(Deserialize)]
struct ObjectItem {
name: Option<String>,
#[serde(rename = "typeName")]
type_name: Option<String>,
}
let query_string = {
let mut builder = UrlParamsBuilder::new();
builder = builder.add_optional("from", from);
builder = builder.add_optional("to", to);
builder = builder.add_optional("filter", filter);
if let Some(l) = limit {
builder = builder.add("limit", &l.to_string());
}
let params = builder.finish();
if params.is_empty() {
String::new()
} else {
format!("?{}", params)
}
};
let response: AuditResponse = ctx
.client
.get(&format!("/rest/api/3/auditing/record{}", query_string))
.await
.context("Failed to list audit records")?;
#[derive(Serialize)]
struct Row<'a> {
id: i64,
summary: &'a str,
category: &'a str,
object_type: &'a str,
object_name: &'a str,
author: &'a str,
created: &'a str,
}
let rows: Vec<Row<'_>> = response
.records
.iter()
.map(|r| Row {
id: r.id,
summary: r.summary.as_str(),
category: r.category.as_str(),
object_type: r.object_item.type_name.as_deref().unwrap_or(""),
object_name: r.object_item.name.as_deref().unwrap_or(""),
author: r.author_key.as_deref().unwrap_or(""),
created: r.created.as_str(),
})
.collect();
ctx.renderer.render(&rows)
}
pub async fn export_audit_records(
ctx: &JiraContext<'_>,
from: Option<&str>,
to: Option<&str>,
filter: Option<&str>,
output: &std::path::PathBuf,
format: ExportFormat,
) -> Result<()> {
#[derive(Deserialize, Serialize)]
struct AuditResponse {
records: Vec<serde_json::Value>,
}
let query_string = {
let builder = UrlParamsBuilder::new()
.add_optional("from", from)
.add_optional("to", to)
.add_optional("filter", filter);
let params = builder.finish();
if params.is_empty() {
String::new()
} else {
format!("?{}", params)
}
};
let response: AuditResponse = ctx
.client
.get(&format!("/rest/api/3/auditing/record{}", query_string))
.await
.context("Failed to export audit records")?;
match format {
ExportFormat::Json => {
let json_str = serde_json::to_string_pretty(&response.records)?;
fs::write(output, json_str)?;
}
ExportFormat::Csv => {
let mut wtr = csv::Writer::from_path(output)?;
wtr.write_record([
"id",
"summary",
"category",
"object_type",
"object_name",
"author",
"created",
])?;
for record in &response.records {
let id = record
.get("id")
.and_then(|v| v.as_i64())
.unwrap_or(0)
.to_string();
let summary = record.get("summary").and_then(|v| v.as_str()).unwrap_or("");
let category = record
.get("category")
.and_then(|v| v.as_str())
.unwrap_or("");
let object_type = record
.get("objectItem")
.and_then(|o| o.get("typeName"))
.and_then(|v| v.as_str())
.unwrap_or("");
let object_name = record
.get("objectItem")
.and_then(|o| o.get("name"))
.and_then(|v| v.as_str())
.unwrap_or("");
let author = record
.get("authorKey")
.and_then(|v| v.as_str())
.unwrap_or("");
let created = record.get("created").and_then(|v| v.as_str()).unwrap_or("");
wtr.write_record([
id.as_str(),
summary,
category,
object_type,
object_name,
author,
created,
])?;
}
wtr.flush()?;
}
}
println!(
"✅ Exported {} audit records to {}",
response.records.len(),
output.display()
);
Ok(())
}
#[derive(Debug, Clone)]
pub enum ExportFormat {
Json,
Csv,
}