Skip to main content

reinhardt_admin/server/
list.rs

1//! List view Server Function
2//!
3//! Provides list view operations for admin models.
4
5#[cfg(server)]
6use super::admin_auth::AdminAuthenticatedUser;
7use crate::adapters::{
8	AdminDatabase, AdminRecord, AdminSite, ColumnInfo, FilterInfo, FilterType, ListResponse,
9	ModelAdmin,
10};
11#[cfg(server)]
12use crate::core::{AdminDatabaseKey, AdminSiteKey};
13#[cfg(server)]
14use reinhardt_db::orm::{Filter, FilterCondition, FilterOperator, FilterValue};
15#[cfg(server)]
16use reinhardt_di::Depends;
17use reinhardt_pages::server_fn::{ServerFnError, server_fn};
18use std::sync::Arc;
19
20#[cfg(server)]
21use super::error::MapServerFnError;
22#[cfg(server)]
23use super::limits::MAX_PAGE_SIZE;
24#[cfg(server)]
25use crate::server::type_inference::{
26	get_field_metadata, infer_admin_field_type, infer_filter_type,
27};
28#[cfg(server)]
29use reinhardt_utils::utils_core::text::humanize_field_name;
30
31#[cfg(server)]
32fn build_filters(model_admin: &Arc<dyn ModelAdmin>) -> Vec<FilterInfo> {
33	let table_name = model_admin.table_name();
34	model_admin
35		.list_filter()
36		.iter()
37		.map(|field| {
38			// Infer filter type from field metadata in global registry
39			let filter_type = get_field_metadata(table_name, field)
40				.map(|meta| {
41					let admin_type = infer_admin_field_type(&meta.field_type);
42					infer_filter_type(&admin_type)
43				})
44				.unwrap_or(FilterType::Boolean);
45
46			FilterInfo {
47				field: field.to_string(),
48				title: humanize_field_name(field),
49				filter_type,
50				current_value: None,
51			}
52		})
53		.collect()
54}
55
56#[cfg(server)]
57fn build_columns(model_admin: &Arc<dyn ModelAdmin>) -> Vec<ColumnInfo> {
58	model_admin
59		.list_display()
60		.iter()
61		.map(|field| ColumnInfo {
62			field: field.to_string(),
63			label: humanize_field_name(field),
64			sortable: true,
65		})
66		.collect()
67}
68
69/// Get list view data with search, filters, sorting, and pagination
70///
71/// Retrieves a paginated list of records with optional search across multiple fields,
72/// field-specific filters, and custom ordering. Returns the records along with
73/// pagination metadata and available filter/column information.
74///
75/// # Server Function
76///
77/// This function is automatically exposed as an HTTP endpoint by the `#[server_fn]` macro.
78/// AdminSite and AdminDatabase dependencies are automatically injected via the DI system.
79///
80/// # Authentication
81///
82/// Requires authentication and view permission for the model.
83///
84/// # Example
85///
86/// ```ignore
87/// use reinhardt_admin::server::get_list;
88/// use reinhardt_admin::types::ListQueryParams;
89/// use std::collections::HashMap;
90///
91/// // Client-side usage (automatically generates HTTP request)
92/// let params = ListQueryParams {
93///     search: Some("alice".to_string()),
94///     filters: HashMap::new(),
95///     sort_by: Some("created_at".to_string()),
96///     page: Some(1),
97///     page_size: Some(25),
98/// };
99/// let response = get_list("User".to_string(), params).await?;
100/// println!("Found {} users", response.count);
101/// ```
102#[server_fn]
103pub async fn get_list(
104	model_name: String,
105	params: crate::adapters::ListQueryParams,
106	#[inject] site: Depends<AdminSiteKey, AdminSite>,
107	#[inject] db: Depends<AdminDatabaseKey, AdminDatabase>,
108	#[inject] AdminAuthenticatedUser(user): AdminAuthenticatedUser,
109) -> Result<crate::adapters::ListResponse, ServerFnError> {
110	// Get model admin and check permission
111	let model_admin = site.get_model_admin(&model_name).map_server_fn_error()?;
112	if !model_admin.has_view_permission(user.as_ref()).await {
113		return Err(ServerFnError::server(403, "Permission denied"));
114	}
115
116	// Build search condition (OR across search fields)
117	let mut filter_condition: Option<FilterCondition> = None;
118	if let Some(search) = params.search.as_ref() {
119		let search_fields = model_admin.search_fields();
120		if !search_fields.is_empty() && !search.is_empty() {
121			let search_filters: Vec<FilterCondition> = search_fields
122				.iter()
123				.map(|field| {
124					FilterCondition::Single(Filter::new(
125						field.to_string(),
126						FilterOperator::Contains,
127						FilterValue::String(search.clone()),
128					))
129				})
130				.collect();
131
132			if !search_filters.is_empty() {
133				filter_condition = Some(FilterCondition::Or(search_filters));
134			}
135		}
136	}
137
138	// Build additional filters (AND logic)
139	// Only accept filter fields that are explicitly defined in model_admin.list_filter()
140	let allowed_filter_fields = model_admin.list_filter();
141	let mut additional_filters = Vec::new();
142	for (field, value) in params.filters.iter() {
143		if !allowed_filter_fields.contains(&field.as_str()) {
144			return Err(ServerFnError::server(
145				400,
146				format!(
147					"Unknown filter field '{}'. Allowed filter fields: {:?}",
148					field, allowed_filter_fields
149				),
150			));
151		}
152		additional_filters.push(Filter::new(
153			field.clone(),
154			FilterOperator::Eq,
155			FilterValue::String(value.clone()),
156		));
157	}
158
159	// Determine sort field
160	let sort_by = params
161		.sort_by
162		.as_deref()
163		.or_else(|| model_admin.ordering().first().copied());
164
165	// Validate sort_by against allowed fields to prevent arbitrary column access
166	if let Some(sort_field) = sort_by {
167		let raw_field = sort_field.strip_prefix('-').unwrap_or(sort_field);
168		let allowed_sort_fields = model_admin.list_display();
169		if !allowed_sort_fields.contains(&raw_field) {
170			return Err(ServerFnError::server(
171				400,
172				format!(
173					"Unknown sort field '{}'. Allowed sort fields: {:?}",
174					raw_field, allowed_sort_fields
175				),
176			));
177		}
178	}
179
180	// Calculate pagination with upper bound enforcement
181	let page = params.page.unwrap_or(1).max(1); // Ensure page is at least 1
182	let page_size = params
183		.page_size
184		.unwrap_or_else(|| {
185			let admin_settings = crate::settings::get_admin_settings();
186			model_admin
187				.list_per_page()
188				.unwrap_or(admin_settings.list_per_page) as u64
189		})
190		.min(MAX_PAGE_SIZE); // Enforce maximum page size to prevent memory exhaustion
191	let offset = (page - 1) * page_size;
192
193	// Fetch page data and total count in one query for the common non-empty page path.
194	let (results, count) = db
195		.list_with_condition_and_count::<AdminRecord>(
196			model_admin.table_name(),
197			filter_condition.as_ref(),
198			additional_filters,
199			sort_by,
200			offset,
201			page_size,
202		)
203		.await
204		.map_server_fn_error()?;
205
206	// Calculate total pages
207	let total_pages = if count > 0 {
208		count.div_ceil(page_size)
209	} else {
210		1
211	};
212
213	Ok(ListResponse {
214		model_name,
215		count,
216		page,
217		page_size,
218		total_pages,
219		results,
220		available_filters: Some(build_filters(&model_admin)),
221		columns: Some(build_columns(&model_admin)),
222	})
223}