Skip to main content

reinhardt_admin/server/
export.rs

1//! Export operation Server Function
2//!
3//! Provides export operations for admin models.
4
5#[cfg(server)]
6use super::admin_auth::AdminAuthenticatedUser;
7use crate::adapters::{AdminDatabase, AdminRecord, AdminSite, ExportFormat, ExportResponse};
8#[cfg(server)]
9use crate::core::{AdminDatabaseKey, AdminSiteKey};
10#[cfg(server)]
11use reinhardt_di::Depends;
12#[cfg(server)]
13use reinhardt_pages::server_fn::ServerFnRequest;
14use reinhardt_pages::server_fn::{ServerFnError, server_fn};
15
16#[cfg(server)]
17use super::error::{AdminAuth, MapServerFnError, ModelPermission};
18#[cfg(server)]
19use super::limits::MAX_EXPORT_RECORDS;
20
21/// Serialize records as delimiter-separated values (CSV or TSV).
22///
23/// Uses `BTreeMap` for consistent column ordering across records and
24/// `write_record` for compatibility with the csv crate (which does not
25/// support serializing maps via `serialize`).
26#[cfg(server)]
27fn serialize_delimited(
28	results: &[std::collections::HashMap<String, serde_json::Value>],
29	delimiter: u8,
30) -> Result<Vec<u8>, String> {
31	use std::collections::BTreeMap;
32
33	if results.is_empty() {
34		return Ok(Vec::new());
35	}
36
37	let mut wtr = csv::WriterBuilder::new()
38		.delimiter(delimiter)
39		.from_writer(vec![]);
40
41	// Convert all records to BTreeMap for consistent column ordering
42	let flat_records: Vec<BTreeMap<String, String>> = results
43		.iter()
44		.map(|record| {
45			record
46				.iter()
47				.map(|(k, v)| {
48					let s = match v {
49						serde_json::Value::String(s) => s.clone(),
50						serde_json::Value::Null => String::new(),
51						other => other.to_string(),
52					};
53					(k.clone(), s)
54				})
55				.collect()
56		})
57		.collect();
58
59	// Write header row from first record's keys
60	let headers: Vec<&str> = flat_records[0].keys().map(|k| k.as_str()).collect();
61	wtr.write_record(&headers)
62		.map_err(|e| format!("header serialization failed: {}", e))?;
63
64	// Write each record's values in header order
65	for record in &flat_records {
66		let values: Vec<&str> = headers
67			.iter()
68			.map(|h| record.get(*h).map(|v| v.as_str()).unwrap_or(""))
69			.collect();
70		wtr.write_record(&values)
71			.map_err(|e| format!("record serialization failed: {}", e))?;
72	}
73
74	wtr.into_inner()
75		.map_err(|e| format!("writer flush failed: {}", e))
76}
77
78/// Export model data in various formats
79///
80/// Exports all records from a model table in the specified format (JSON, CSV, TSV).
81/// Returns the exported data as binary content with appropriate content type and filename.
82///
83/// # Server Function
84///
85/// This function is automatically exposed as an HTTP endpoint by the `#[server_fn]` macro.
86/// AdminSite and AdminDatabase dependencies are automatically injected via the DI system.
87///
88/// # Authentication
89///
90/// Requires staff (admin) permission and view permission for the model.
91///
92/// # Example
93///
94/// ```ignore
95/// use reinhardt_admin::server::export_data;
96/// use reinhardt_admin::types::ExportFormat;
97///
98/// // Client-side usage (automatically generates HTTP request)
99/// let response = export_data("User".to_string(), ExportFormat::JSON).await?;
100/// println!("Downloaded {}", response.filename);
101/// ```
102#[server_fn]
103pub async fn export_data(
104	model_name: String,
105	format: crate::adapters::ExportFormat,
106	#[inject] site: Depends<AdminSiteKey, AdminSite>,
107	#[inject] db: Depends<AdminDatabaseKey, AdminDatabase>,
108	#[inject] http_request: ServerFnRequest,
109	#[inject] AdminAuthenticatedUser(user): AdminAuthenticatedUser,
110) -> Result<crate::adapters::ExportResponse, ServerFnError> {
111	// Authentication and authorization check
112	let auth = AdminAuth::from_request(&http_request);
113	let model_admin = site.get_model_admin(&model_name).map_server_fn_error()?;
114	auth.require_model_permission(model_admin.as_ref(), user.as_ref(), ModelPermission::View)
115		.await?;
116	let table_name = model_admin.table_name();
117
118	// Query total count to detect truncation
119	let total_count = db
120		.count::<AdminRecord>(table_name, vec![])
121		.await
122		.map_server_fn_error()?;
123	let truncated = total_count > MAX_EXPORT_RECORDS;
124
125	if truncated {
126		tracing::warn!(
127			"Export for model '{}' truncated: {} total records, limit is {}",
128			model_name,
129			total_count,
130			MAX_EXPORT_RECORDS
131		);
132	}
133
134	// Fetch records with export limit to prevent memory exhaustion
135	let results = db
136		.list::<AdminRecord>(table_name, vec![], 0, MAX_EXPORT_RECORDS)
137		.await
138		.map_server_fn_error()?;
139
140	// Serialize based on format
141	let (data, filename, content_type) = match format {
142		ExportFormat::JSON => {
143			let json = serde_json::to_vec_pretty(&results).map_err(|e| {
144				ServerFnError::serialization(format!("JSON serialization failed: {}", e))
145			})?;
146			(
147				json,
148				format!("{}.json", model_name.to_lowercase()),
149				"application/json",
150			)
151		}
152		ExportFormat::CSV => {
153			let data = serialize_delimited(&results, b',').map_err(|e| {
154				ServerFnError::serialization(format!("CSV serialization failed: {}", e))
155			})?;
156
157			(
158				data,
159				format!("{}.csv", model_name.to_lowercase()),
160				"text/csv",
161			)
162		}
163		ExportFormat::TSV => {
164			let data = serialize_delimited(&results, b'\t').map_err(|e| {
165				ServerFnError::serialization(format!("TSV serialization failed: {}", e))
166			})?;
167
168			(
169				data,
170				format!("{}.tsv", model_name.to_lowercase()),
171				"text/tab-separated-values",
172			)
173		}
174		ExportFormat::Excel => {
175			return Err(ServerFnError::application(
176				"Excel export format is not supported",
177			));
178		}
179		ExportFormat::XML => {
180			return Err(ServerFnError::application(
181				"XML export format is not supported",
182			));
183		}
184	};
185
186	Ok(ExportResponse {
187		data,
188		filename,
189		content_type: content_type.to_string(),
190		truncated,
191		total_count: Some(total_count),
192	})
193}