1use crate::extractors::{JsonOrForm, StrictJsonOrForm};
9use serde::{de::DeserializeOwned, Deserialize, Serialize};
10use std::collections::HashMap;
11use std::sync::Arc;
12
13#[derive(Debug, Serialize, Deserialize)]
19#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
20pub struct ApiResponse<T> {
21 pub success: bool,
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub data: Option<T>,
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub message: Option<String>,
26 #[serde(skip_serializing_if = "Option::is_none")]
27 pub error: Option<String>,
28 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub violations: Option<Vec<crate::violation::Violation>>,
33}
34
35impl<T> ApiResponse<T> {
36 pub fn rejected(error: impl Into<String>, violations: Vec<crate::violation::Violation>) -> Self {
39 Self {
40 success: false,
41 data: None,
42 message: None,
43 error: Some(error.into()),
44 violations: (!violations.is_empty()).then_some(violations),
45 }
46 }
47
48 pub fn success(data: T, message: Option<String>) -> Self {
50 Self {
51 success: true,
52 data: Some(data),
53 message,
54 error: None,
55 violations: None,
56 }
57 }
58
59 pub fn ok(data: T) -> Self {
61 Self {
62 success: true,
63 data: Some(data),
64 message: None,
65 error: None,
66 violations: None,
67 }
68 }
69
70 pub fn success_with_message(data: T, message: impl Into<String>) -> Self {
71 Self {
72 success: true,
73 data: Some(data),
74 message: Some(message.into()),
75 error: None,
76 violations: None,
77 }
78 }
79
80 pub fn error(error: impl Into<String>) -> Self {
81 Self {
82 success: false,
83 data: None,
84 message: None,
85 error: Some(error.into()),
86 violations: None,
87 }
88 }
89
90 pub fn not_found(entity: &str, id: &str) -> Self {
91 Self {
92 success: false,
93 data: None,
94 message: None,
95 error: Some(format!("{} with id '{}' not found", entity, id)),
96 violations: None,
97 }
98 }
99}
100
101pub const INTERNAL_ERROR_MESSAGE: &str = "An internal error occurred. It has been logged.";
104
105fn error_at<T>(
109 status: axum::http::StatusCode,
110 violations: Option<Vec<crate::violation::Violation>>,
111 err: &impl std::fmt::Display,
112) -> (axum::http::StatusCode, axum::Json<ApiResponse<T>>) {
113 if status.is_server_error() {
114 tracing::error!(target: "backbone::http", status = status.as_u16(), error = %err, "request failed");
115 return (status, axum::Json(ApiResponse::error(INTERNAL_ERROR_MESSAGE)));
116 }
117 match violations {
118 Some(v) if !v.is_empty() => (status, axum::Json(ApiResponse::rejected(err.to_string(), v))),
119 _ => (status, axum::Json(ApiResponse::error(err.to_string()))),
120 }
121}
122
123pub fn service_error_status(err: &crate::service::ServiceError) -> axum::http::StatusCode {
129 use crate::persistence::traits::RepositoryError;
130 use crate::service::ServiceError;
131 use axum::http::StatusCode;
132 match err {
133 ServiceError::Violations(_) | ServiceError::Repository(RepositoryError::Violations(_)) => {
134 StatusCode::UNPROCESSABLE_ENTITY
135 }
136 ServiceError::Validation(_)
137 | ServiceError::Repository(RepositoryError::ValidationError(_)) => StatusCode::BAD_REQUEST,
138 ServiceError::NotFound | ServiceError::Repository(RepositoryError::NotFound) => {
139 StatusCode::NOT_FOUND
140 }
141 ServiceError::AlreadyExists(_)
142 | ServiceError::Repository(RepositoryError::AlreadyExists(_))
143 | ServiceError::Repository(RepositoryError::Conflict(_)) => StatusCode::CONFLICT,
144 _ => StatusCode::INTERNAL_SERVER_ERROR,
145 }
146}
147
148pub fn service_error_response<T>(
153 err: &crate::service::ServiceError,
154) -> (axum::http::StatusCode, axum::Json<ApiResponse<T>>) {
155 use crate::persistence::traits::RepositoryError;
156 use crate::service::ServiceError;
157 let violations = match err {
158 ServiceError::Violations(v) | ServiceError::Repository(RepositoryError::Violations(v)) => {
159 Some(v.clone())
160 }
161 _ => None,
162 };
163 error_at(service_error_status(err), violations, err)
164}
165
166#[cfg(test)]
169fn write_error<T>(
170 violations: Option<Vec<crate::violation::Violation>>,
171 err: &impl std::fmt::Display,
172 fallback: axum::http::StatusCode,
173) -> (axum::http::StatusCode, axum::Json<ApiResponse<T>>) {
174 let status = if violations.is_some() { axum::http::StatusCode::UNPROCESSABLE_ENTITY } else { fallback };
175 error_at(status, violations, err)
176}
177
178fn invalid_id<T>(id: &str, path: &str) -> Option<(axum::http::StatusCode, axum::Json<ApiResponse<T>>)> {
182 if uuid::Uuid::parse_str(id).is_ok() {
183 return None;
184 }
185 let violation = crate::violation::Violation::new(
186 path,
187 "invalid_id",
188 format!("`{id}` is not a valid record id"),
189 );
190 Some((
191 axum::http::StatusCode::BAD_REQUEST,
192 axum::Json(ApiResponse::rejected(format!("invalid id: `{id}` is not a valid record id"), vec![violation])),
193 ))
194}
195
196fn invalid_ids<'a, T>(
199 ids: impl IntoIterator<Item = &'a str>,
200 path_of: impl Fn(usize) -> String,
201) -> Option<(axum::http::StatusCode, axum::Json<ApiResponse<T>>)> {
202 let violations: Vec<crate::violation::Violation> = ids
203 .into_iter()
204 .enumerate()
205 .filter(|(_, id)| uuid::Uuid::parse_str(id).is_err())
206 .map(|(i, id)| {
207 crate::violation::Violation::new(path_of(i), "invalid_id", format!("`{id}` is not a valid record id"))
208 })
209 .collect();
210 if violations.is_empty() {
211 return None;
212 }
213 Some((
214 axum::http::StatusCode::BAD_REQUEST,
215 axum::Json(ApiResponse::rejected("invalid id in the batch", violations)),
216 ))
217}
218
219#[derive(Debug, Deserialize, Default, Clone)]
221#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
222pub struct ListQueryParams {
223 #[serde(default = "default_page")]
224 pub page: u32,
225 #[serde(default = "default_limit")]
226 pub limit: u32,
227 #[serde(default)]
228 pub sort_by: Option<String>,
229 #[serde(default)]
230 pub sort_order: Option<String>,
231 #[serde(default)]
232 pub search: Option<String>,
233 #[serde(default)]
234 pub status: Option<String>,
235 #[serde(flatten)]
236 pub filters: HashMap<String, String>,
237}
238
239fn default_page() -> u32 { 1 }
240fn default_limit() -> u32 { 20 }
241
242const RESERVED_QUERY_KEYS: [&str; 3] = ["fields", "include", "with"];
244
245#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct HistoryEntry {
260 pub occurred_at: String,
261 pub action: String,
263 pub actor: String,
264 pub changed: serde_json::Value,
265 pub reason: Option<String>,
266 pub correlation_id: Option<String>,
267}
268
269#[async_trait::async_trait]
276pub trait HistoryProvider: Send + Sync {
277 async fn history(
285 &self,
286 table: &str,
287 id: &str,
288 limit: u32,
289 offset: u32,
290 ) -> Result<Option<Vec<HistoryEntry>>, String>;
291}
292
293const AGGREGATE_QUERY_KEYS: [&str; 7] =
299 ["group_by", "sum", "avg", "min", "max", "group_limit", "group_label"];
300
301fn aggregate_spec(params: &ListQueryParams) -> backbone_orm::repository::AggregateSpec {
307 use backbone_orm::repository::{AggregateFn, AggregateSpec};
308
309 let mut reductions = Vec::new();
310 for (key, func) in [
311 ("sum", AggregateFn::Sum),
312 ("avg", AggregateFn::Avg),
313 ("min", AggregateFn::Min),
314 ("max", AggregateFn::Max),
315 ] {
316 if let Some(raw) = params.filters.get(key) {
317 for field in raw.split(',').map(str::trim).filter(|f| !f.is_empty()) {
318 reductions.push((func, field.to_string()));
319 }
320 }
321 }
322
323 AggregateSpec {
324 group_by: params
325 .filters
326 .get("group_by")
327 .map(|g| g.trim().to_string())
328 .filter(|g| !g.is_empty()),
329 reductions,
330 group_limit: params
331 .filters
332 .get("group_limit")
333 .and_then(|l| l.trim().parse::<usize>().ok())
334 .unwrap_or(0),
335 label_field: params
336 .filters
337 .get("group_label")
338 .map(|l| l.trim().to_string())
339 .filter(|l| !l.is_empty()),
340 label_relation: None,
341 }
342}
343
344fn named_secret(params: &ListQueryParams, secret_fields: &[&str]) -> Option<String> {
351 if secret_fields.is_empty() {
352 return None;
353 }
354 let secrets: Vec<String> = secret_fields.iter().map(|f| camel_to_snake_case(f)).collect();
355 const COLUMN_LISTS: [&str; 10] = [
356 "orderby", "sort", "searchfields", "search_fields", "group_by", "sum", "avg", "min", "max", "group_label",
357 ];
358 let mut names: Vec<String> = Vec::new();
359 for (key, value) in ¶ms.filters {
360 match key.find('[') {
361 Some(open) => {
362 names.push(key[..open].to_string());
363 names.push(key[open + 1..].trim_end_matches(']').to_string());
364 }
365 None => names.push(key.clone()),
366 }
367 if COLUMN_LISTS.contains(&key.to_ascii_lowercase().as_str()) {
368 names.extend(value.split(',').map(|v| v.trim().trim_start_matches('-').to_string()));
369 }
370 }
371 names.extend(params.sort_by.iter().cloned());
372 names
373 .into_iter()
374 .find(|n| secrets.contains(&camel_to_snake_case(n.trim())))
375}
376
377fn secret_named_message(field: &str) -> String {
379 format!("'{field}' is a secret field: it cannot be filtered, sorted, searched or aggregated")
380}
381
382fn repository_filters(params: &ListQueryParams) -> HashMap<String, String> {
394 let mut filters = params.filters.clone();
395 for key in RESERVED_QUERY_KEYS {
396 filters.remove(key);
397 }
398 filters.retain(|key, _| !backbone_orm::is_internal_key(key));
401 if let Some(search) = params.search.clone() {
402 filters.insert("search".to_string(), search);
403 }
404 if let Some(status) = params.status.clone() {
405 filters.insert("status".to_string(), status);
406 }
407 filters
408}
409
410fn aggregate_filters(params: &ListQueryParams) -> HashMap<String, String> {
413 let mut filters = repository_filters(params);
414 for key in AGGREGATE_QUERY_KEYS {
415 filters.remove(key);
416 }
417 filters
418}
419
420fn sparse_fields(query: &HashMap<String, String>) -> Vec<String> {
421 query
422 .get("fields")
423 .map(|s| {
424 s.split(',')
425 .map(str::trim)
426 .filter(|f| !f.is_empty())
427 .map(str::to_string)
428 .collect()
429 })
430 .unwrap_or_default()
431}
432
433fn to_response_value<R: Serialize>(r: R) -> serde_json::Value {
436 serde_json::to_value(r).unwrap_or(serde_json::Value::Null)
437}
438
439fn project_sparse(mut value: serde_json::Value, fields: &[String]) -> serde_json::Value {
443 if fields.is_empty() {
444 return value;
445 }
446 if let serde_json::Value::Object(map) = &mut value {
447 map.retain(|k, _| k == "id" || fields.iter().any(|f| f == k));
448 }
449 value
450}
451
452#[derive(Debug, Clone, Copy, PartialEq, Eq)]
475pub enum AccessScope {
476 Platform,
478 Company(uuid::Uuid),
480}
481
482impl AccessScope {
483 pub fn company(&self) -> Option<uuid::Uuid> {
489 match self {
490 AccessScope::Platform => None,
491 AccessScope::Company(id) => Some(*id),
492 }
493 }
494}
495
496fn strip_secrets(mut value: serde_json::Value, secret_fields: &[&str]) -> serde_json::Value {
506 if let serde_json::Value::Object(map) = &mut value {
507 for f in secret_fields {
508 map.remove(*f);
509 }
510 }
511 value
512}
513
514fn secure<E: backbone_orm::EntityRepoMeta>(
518 value: serde_json::Value,
519 scope: Option<&AccessScope>,
520) -> serde_json::Value {
521 apply_field_security(
522 strip_secrets(value, E::secret_fields()),
523 scope,
524 E::private_fields(),
525 E::owner_field(),
526 )
527}
528
529pub fn without_secrets<E: backbone_orm::EntityRepoMeta, R: Serialize>(response: R) -> serde_json::Value {
533 secure::<E>(to_response_value(response), None)
534}
535
536fn related_row<E: backbone_orm::EntityRepoMeta>(
540 relation: &str,
541 table: &str,
542 obj: serde_json::Value,
543) -> serde_json::Value {
544 let row = strip_secrets(camelize_keys(obj), E::relation_secret_fields(relation));
545 strip_secrets(row, &backbone_orm::secret_registry::secret_fields_of(table))
547}
548
549fn apply_field_security(
550 mut value: serde_json::Value,
551 scope: Option<&AccessScope>,
552 private_fields: &[&str],
553 owner_field: Option<&str>,
554) -> serde_json::Value {
555 if private_fields.is_empty() {
556 return value;
557 }
558 let can_see_private = match scope {
559 Some(AccessScope::Platform) => true,
560 Some(AccessScope::Company(id)) => owner_field
561 .and_then(|f| value.get(f))
562 .and_then(|v| v.as_str())
563 .and_then(|owner| uuid::Uuid::parse_str(owner).ok())
567 .is_some_and(|owner| owner == *id),
568 None => false,
569 };
570 if !can_see_private {
571 if let serde_json::Value::Object(map) = &mut value {
572 for f in private_fields {
573 map.remove(*f);
574 }
575 }
576 }
577 value
578}
579
580fn include_relations(query: &HashMap<String, String>) -> Vec<String> {
582 query
583 .get("include")
584 .or_else(|| query.get("with"))
585 .map(|s| {
586 s.split(',')
587 .map(str::trim)
588 .filter(|s| !s.is_empty())
589 .map(str::to_string)
590 .collect()
591 })
592 .unwrap_or_default()
593}
594
595fn snake_to_camel(s: &str) -> String {
596 let mut out = String::with_capacity(s.len());
597 let mut upper = false;
598 for c in s.chars() {
599 if c == '_' {
600 upper = true;
601 } else if upper {
602 out.extend(c.to_uppercase());
603 upper = false;
604 } else {
605 out.push(c);
606 }
607 }
608 out
609}
610
611fn camelize_keys(v: serde_json::Value) -> serde_json::Value {
614 match v {
615 serde_json::Value::Object(m) => serde_json::Value::Object(
616 m.into_iter().map(|(k, val)| (snake_to_camel(&k), val)).collect(),
617 ),
618 other => other,
619 }
620}
621
622async fn expand_includes<S, E, C, U>(
632 service: &S,
633 rows: &mut [serde_json::Value],
634 includes: &[String],
635) where
636 S: CrudService<E, C, U>,
637 E: backbone_orm::EntityRepoMeta + Send + Sync + 'static,
638 C: Send + Sync + 'static,
639 U: Send + Sync + 'static,
640{
641 if includes.is_empty() || rows.is_empty() {
642 return;
643 }
644 for (rel_name, table, fk_field) in E::relations() {
645 if !includes.iter().any(|i| i == rel_name) {
646 continue;
647 }
648 let mut ids: Vec<String> = rows
649 .iter()
650 .filter_map(|r| r.get(fk_field).and_then(|v| v.as_str()).map(str::to_string))
651 .collect();
652 ids.sort();
653 ids.dedup();
654 if ids.is_empty() {
655 continue;
656 }
657 let related = service.fetch_related_json(table, &ids).await;
658 let mut by_id: HashMap<String, serde_json::Value> = HashMap::new();
659 for obj in related {
660 if let Some(id) = obj.get("id").and_then(|v| v.as_str()).map(str::to_string) {
661 by_id.insert(id, related_row::<E>(rel_name, table, obj));
662 }
663 }
664 for r in rows.iter_mut() {
665 let related_obj = r
666 .get(fk_field)
667 .and_then(|v| v.as_str())
668 .and_then(|id| by_id.get(id).cloned())
669 .unwrap_or(serde_json::Value::Null);
670 if let serde_json::Value::Object(m) = r {
671 m.insert((*rel_name).to_string(), related_obj);
672 }
673 }
674 }
675}
676
677pub const MAX_PER_PAGE: u32 = 100;
681
682pub const MAX_PAGINATION_OFFSET: u32 = 10_000;
687
688fn pagination_depth_error(page: u32, limit: u32) -> Option<String> {
694 let effective_limit = limit.clamp(1, MAX_PER_PAGE);
695 let offset = page.max(1).saturating_sub(1).saturating_mul(effective_limit);
696 if offset > MAX_PAGINATION_OFFSET {
697 Some(format!(
698 "Result set too deep: offset {offset} exceeds the maximum of \
699 {MAX_PAGINATION_OFFSET}. Please add filters to narrow your search."
700 ))
701 } else {
702 None
703 }
704}
705
706pub use crate::service::MAX_BATCH_SIZE;
712
713fn batch_size_error(count: usize) -> Option<String> {
716 if count > MAX_BATCH_SIZE {
717 Some(format!(
718 "Batch too large: {count} items exceeds the maximum of {MAX_BATCH_SIZE}."
719 ))
720 } else {
721 None
722 }
723}
724
725fn is_bad_query_error(msg: &str) -> bool {
734 let m = msg.to_lowercase();
735 m.contains("does not exist")
736 || m.contains("invalid input syntax")
737 || m.contains("42703")
738 || m.contains("not a column of this entity")
742 || m.contains("needs a numeric column")
743}
744
745#[derive(Debug, Serialize, Deserialize, Clone)]
747#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
748pub struct PaginationResponse {
749 pub total: u64,
750 pub page: u32,
751 pub limit: u32,
752 pub total_pages: u32,
753 #[serde(skip_serializing_if = "Option::is_none")]
757 pub next_cursor: Option<String>,
758 #[serde(skip_serializing_if = "Option::is_none")]
760 pub prev_cursor: Option<String>,
761 #[serde(skip_serializing_if = "Option::is_none")]
763 pub has_more: Option<bool>,
764}
765
766impl PaginationResponse {
767 pub fn new(total: u64, page: u32, limit: u32) -> Self {
768 let total_pages = if limit == 0 { 0 } else { ((total as f64) / (limit as f64)).ceil() as u32 };
769 Self { total, page, limit, total_pages, next_cursor: None, prev_cursor: None, has_more: None }
770 }
771
772 pub fn from_info(info: &backbone_orm::repository::PaginationInfo) -> Self {
776 Self {
777 total: info.total,
778 page: info.page,
779 limit: info.per_page,
780 total_pages: info.total_pages,
781 next_cursor: info.next_cursor.clone(),
782 prev_cursor: info.prev_cursor.clone(),
783 has_more: info.has_more,
784 }
785 }
786}
787
788#[derive(Debug, Serialize)]
790#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
791pub struct PaginatedResponse<T> {
792 pub data: Vec<T>,
793 pub meta: PaginationResponse,
794}
795
796#[derive(Debug, Serialize)]
799#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
800pub struct PaginatedApiResponse<T> {
801 pub success: bool,
802 pub data: Vec<T>,
803 pub meta: PaginationResponse,
804 #[serde(skip_serializing_if = "Option::is_none")]
805 pub error: Option<String>,
806}
807
808impl<T> PaginatedApiResponse<T> {
809 pub fn ok_with_info(data: Vec<T>, info: &backbone_orm::repository::PaginationInfo) -> Self {
812 Self {
813 success: true,
814 data,
815 meta: PaginationResponse::from_info(info),
816 error: None,
817 }
818 }
819
820 pub fn ok(data: Vec<T>, total: u64, page: u32, limit: u32) -> Self {
822 Self {
823 success: true,
824 data,
825 meta: PaginationResponse::new(total, page, limit),
826 error: None,
827 }
828 }
829
830 pub fn from_paginated(resp: PaginatedResponse<T>) -> Self {
832 Self {
833 success: true,
834 data: resp.data,
835 meta: resp.meta,
836 error: None,
837 }
838 }
839
840 pub fn error(error: impl Into<String>) -> Self {
842 Self {
843 success: false,
844 data: Vec::new(),
845 meta: PaginationResponse::new(0, 0, 0),
846 error: Some(error.into()),
847 }
848 }
849}
850
851#[derive(Debug, Serialize, Deserialize)]
853#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
854pub struct BulkCreateRequest<T> {
855 pub items: Vec<T>,
856}
857
858#[derive(Debug, Serialize, Deserialize)]
860#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
861pub struct BulkResponse<T> {
862 pub items: Vec<T>,
863 pub total: usize,
864 pub failed: usize,
865 pub errors: Vec<String>,
866}
867
868#[derive(Debug, Serialize, Deserialize)]
870#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
871pub struct UpsertRequest<T> {
872 pub entity: T,
873 pub create_if_not_exists: bool,
874}
875
876#[derive(Debug, Serialize, Deserialize)]
879#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
880pub struct BatchIdsRequest {
881 pub ids: Vec<String>,
882}
883
884#[derive(Debug, Deserialize)]
887#[serde(bound = "U: DeserializeOwned")]
888#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
889pub struct BulkUpdateItem<U> {
890 pub id: String,
891 #[serde(flatten)]
895 #[cfg_attr(feature = "openapi", schema(value_type = Object))]
896 pub data: U,
897}
898
899#[derive(Debug, Deserialize)]
901#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
902pub struct BulkPatchItem {
903 pub id: String,
904 pub patch: HashMap<String, serde_json::Value>,
905}
906
907#[derive(Debug, Deserialize)]
910#[serde(untagged)]
911#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
912pub enum BulkPatchRequest {
913 Shared {
915 ids: Vec<String>,
916 patch: HashMap<String, serde_json::Value>,
917 },
918 PerItem { items: Vec<BulkPatchItem> },
920}
921
922impl BulkPatchRequest {
923 fn into_items(self) -> Vec<(String, HashMap<String, serde_json::Value>)> {
925 match self {
926 BulkPatchRequest::Shared { ids, patch } => {
927 ids.into_iter().map(|id| (id, patch.clone())).collect()
928 }
929 BulkPatchRequest::PerItem { items } => {
930 items.into_iter().map(|it| (it.id, it.patch)).collect()
931 }
932 }
933 }
934}
935
936#[derive(Debug, Default, Serialize)]
938#[serde(rename_all = "camelCase")]
939#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
940pub struct BulkPreview {
941 pub will_change: Vec<PreviewChange>,
943 pub unchanged: Vec<String>,
945 pub refused: Vec<PreviewRefusal>,
947 pub total: usize,
949}
950
951#[derive(Debug, Serialize)]
953#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
954pub struct PreviewChange {
955 pub id: String,
957 pub changes: Vec<FieldChange>,
959}
960
961#[derive(Debug, Serialize)]
963#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
964pub struct FieldChange {
965 pub field: String,
967 #[cfg_attr(feature = "openapi", schema(value_type = Object))]
969 pub from: serde_json::Value,
970 #[cfg_attr(feature = "openapi", schema(value_type = Object))]
972 pub to: serde_json::Value,
973}
974
975#[derive(Debug, Serialize)]
977#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
978pub struct PreviewRefusal {
979 pub id: String,
981 pub code: String,
983 pub message: String,
985}
986
987type PreviewOutcome = Result<(serde_json::Value, serde_json::Value), (String, String)>;
989
990fn preview_answer(
997 rows: Vec<(String, PreviewOutcome)>,
998 patched: &HashMap<String, Vec<String>>,
999) -> BulkPreview {
1000 let mut out = BulkPreview::default();
1001 for (id, outcome) in rows {
1002 match outcome {
1003 Ok((before, after)) => {
1004 let mut keys = patched.get(&id).cloned().unwrap_or_default();
1005 keys.sort();
1006 let changes: Vec<FieldChange> = keys
1007 .into_iter()
1008 .filter_map(|field| {
1009 let read = |v: &serde_json::Value| {
1010 v.get(snake_to_camel(&field))
1011 .or_else(|| v.get(&field))
1012 .cloned()
1013 .unwrap_or(serde_json::Value::Null)
1014 };
1015 let (from, to) = (read(&before), read(&after));
1016 (from != to).then_some(FieldChange { field, from, to })
1017 })
1018 .collect();
1019 if changes.is_empty() {
1020 out.unchanged.push(id);
1021 } else {
1022 out.will_change.push(PreviewChange { id, changes });
1023 }
1024 }
1025 Err((code, message)) => out.refused.push(PreviewRefusal { id, code, message }),
1026 }
1027 }
1028 out.total = out.will_change.len() + out.unchanged.len() + out.refused.len();
1029 out
1030}
1031
1032#[derive(Debug, Serialize, Deserialize)]
1034#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1035pub struct FilterOptions {
1036 pub filters: HashMap<String, String>,
1037 pub sort_by: Option<String>,
1038 pub sort_order: Option<SortOrder>,
1039}
1040
1041#[derive(Debug, Serialize, Deserialize, Clone, Default)]
1043#[serde(rename_all = "lowercase")]
1044#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1045pub enum SortOrder {
1046 #[default]
1047 Asc,
1048 Desc,
1049}
1050
1051#[derive(Debug, Serialize, Deserialize)]
1053#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1054pub struct ListRequest {
1055 pub page: Option<u32>,
1056 pub limit: Option<u32>,
1057 pub sort_by: Option<String>,
1058 pub sort_order: Option<SortOrder>,
1059 pub filters: Option<HashMap<String, String>>,
1060}
1061
1062impl Default for ListRequest {
1063 fn default() -> Self {
1064 Self {
1065 page: Some(1),
1066 limit: Some(20),
1067 sort_by: None,
1068 sort_order: None,
1069 filters: None,
1070 }
1071 }
1072}
1073
1074#[async_trait::async_trait]
1098pub trait CrudService<Entity, CreateDto, UpdateDto>: Send + Sync
1099where
1100 Entity: Send + Sync + 'static,
1103 CreateDto: Send + Sync + 'static,
1104 UpdateDto: Send + Sync + 'static,
1105{
1106 type Error: std::error::Error + Send + Sync;
1108
1109 fn violations_of(err: &Self::Error) -> Option<Vec<crate::violation::Violation>> {
1112 let _ = err;
1113 None
1114 }
1115
1116 fn status_of(err: &Self::Error) -> Option<axum::http::StatusCode> {
1121 let _ = err;
1122 None
1123 }
1124
1125 fn supports_upsert() -> bool {
1129 true
1130 }
1131
1132 fn entity_name() -> &'static str;
1134
1135 async fn fetch_related_json(
1138 &self,
1139 _table: &str,
1140 _ids: &[String],
1141 ) -> Vec<serde_json::Value> {
1142 Vec::new()
1143 }
1144
1145 async fn list(&self, page: u32, limit: u32, filters: HashMap<String, String>) -> Result<(Vec<Entity>, u64), Self::Error>;
1147
1148 async fn list_with_info(
1152 &self,
1153 page: u32,
1154 limit: u32,
1155 filters: HashMap<String, String>,
1156 ) -> Result<(Vec<Entity>, backbone_orm::repository::PaginationInfo), Self::Error> {
1157 let (rows, total) = self.list(page, limit, filters).await?;
1158 Ok((
1159 rows,
1160 backbone_orm::repository::PaginationInfo::new(page, limit, total),
1161 ))
1162 }
1163
1164 async fn create(&self, dto: CreateDto) -> Result<Entity, Self::Error>;
1166
1167 async fn get_by_id(&self, id: &str) -> Result<Option<Entity>, Self::Error>;
1169
1170 async fn update(&self, id: &str, dto: UpdateDto) -> Result<Option<Entity>, Self::Error>;
1172
1173 async fn partial_update(&self, id: &str, fields: HashMap<String, serde_json::Value>) -> Result<Option<Entity>, Self::Error>;
1175
1176 async fn soft_delete(&self, id: &str) -> Result<bool, Self::Error>;
1178
1179 async fn bulk_create(&self, items: Vec<CreateDto>) -> Result<Vec<Entity>, Self::Error>;
1181
1182 async fn upsert(&self, dto: CreateDto) -> Result<Entity, Self::Error>;
1184
1185 async fn list_deleted(&self, page: u32, limit: u32) -> Result<(Vec<Entity>, u64), Self::Error>;
1187
1188 async fn restore(&self, id: &str) -> Result<Option<Entity>, Self::Error>;
1190
1191 async fn empty_trash(&self) -> Result<u64, Self::Error>;
1193
1194 async fn get_deleted_by_id(&self, id: &str) -> Result<Option<Entity>, Self::Error>;
1196
1197 async fn permanent_delete(&self, id: &str) -> Result<bool, Self::Error>;
1199
1200 async fn list_deleted_filtered(&self, page: u32, limit: u32, filters: HashMap<String, String>) -> Result<(Vec<Entity>, u64), Self::Error>;
1202
1203 async fn count_active(&self) -> Result<u64, Self::Error>;
1205
1206 fn table_name(&self) -> Option<&str> {
1213 None
1214 }
1215
1216 async fn aggregate(
1221 &self,
1222 spec: &backbone_orm::repository::AggregateSpec,
1223 filters: HashMap<String, String>,
1224 ) -> Result<backbone_orm::repository::AggregateResult, Self::Error>;
1225
1226 async fn count_active_filtered(
1239 &self,
1240 filters: HashMap<String, String>,
1241 ) -> Result<u64, Self::Error> {
1242 self.list(1, 1, filters).await.map(|(_, total)| total)
1243 }
1244
1245 async fn count_deleted(&self) -> Result<u64, Self::Error>;
1247
1248 async fn bulk_soft_delete(&self, ids: Vec<String>) -> Result<u64, Self::Error> {
1257 let mut n = 0;
1258 for id in ids {
1259 if self.soft_delete(&id).await? {
1260 n += 1;
1261 }
1262 }
1263 Ok(n)
1264 }
1265
1266 async fn bulk_restore(&self, ids: Vec<String>) -> Result<Vec<Entity>, Self::Error> {
1268 let mut out = Vec::with_capacity(ids.len());
1269 for id in ids {
1270 if let Some(e) = self.restore(&id).await? {
1271 out.push(e);
1272 }
1273 }
1274 Ok(out)
1275 }
1276
1277 async fn bulk_permanent_delete(&self, ids: Vec<String>) -> Result<u64, Self::Error> {
1279 let mut n = 0;
1280 for id in ids {
1281 if self.permanent_delete(&id).await? {
1282 n += 1;
1283 }
1284 }
1285 Ok(n)
1286 }
1287
1288 async fn restore_all(&self) -> Result<u64, Self::Error> {
1293 Ok(0)
1294 }
1295
1296 async fn bulk_update(&self, items: Vec<(String, UpdateDto)>) -> Result<Vec<Entity>, Self::Error> {
1298 let mut out = Vec::with_capacity(items.len());
1299 for (id, dto) in items {
1300 if let Some(e) = self.update(&id, dto).await? {
1301 out.push(e);
1302 }
1303 }
1304 Ok(out)
1305 }
1306
1307 async fn bulk_partial_update(
1309 &self,
1310 items: Vec<(String, HashMap<String, serde_json::Value>)>,
1311 ) -> Result<Vec<Entity>, Self::Error> {
1312 let mut out = Vec::with_capacity(items.len());
1313 for (id, fields) in items {
1314 if let Some(e) = self.partial_update(&id, fields).await? {
1315 out.push(e);
1316 }
1317 }
1318 Ok(out)
1319 }
1320
1321 async fn preview_bulk_partial_update(
1326 &self,
1327 items: Vec<(String, HashMap<String, serde_json::Value>)>,
1328 ) -> Result<Option<Vec<BulkPreviewRow<Entity, Self::Error>>>, Self::Error> {
1329 let _ = items;
1330 Ok(None)
1331 }
1332}
1333
1334pub struct BulkPreviewRow<Entity, Err> {
1337 pub id: String,
1339 pub outcome: Result<(Entity, Entity), Err>,
1341}
1342
1343pub struct BackboneCrudHandler<S, E, C, U, R>
1366where
1367 S: CrudService<E, C, U> + 'static,
1368 E: Serialize + Send + Sync + 'static,
1369 C: DeserializeOwned + Send + Sync + 'static,
1370 U: DeserializeOwned + Send + Sync + 'static,
1371 R: From<E> + Serialize + Send + Sync + 'static,
1372{
1373 service: Arc<S>,
1374 _phantom: std::marker::PhantomData<(E, C, U, R)>,
1375}
1376
1377impl<S, E, C, U, R> BackboneCrudHandler<S, E, C, U, R>
1378where
1379 S: CrudService<E, C, U> + 'static,
1380 E: Serialize + Send + Sync + Clone + backbone_orm::EntityRepoMeta + 'static,
1381 C: DeserializeOwned + Send + Sync + 'static,
1382 U: DeserializeOwned + Send + Sync + 'static,
1383 R: From<E> + Serialize + Send + Sync + 'static,
1384{
1385 pub fn new(service: Arc<S>) -> Self {
1386 Self {
1387 service,
1388 _phantom: std::marker::PhantomData,
1389 }
1390 }
1391
1392 pub fn routes(service: Arc<S>, base_path: &str) -> axum::Router<()>
1416 where
1417 S: Clone,
1418 {
1419 Self::read_routes(service.clone(), base_path)
1420 .merge(Self::write_routes(service, base_path))
1421 }
1422
1423 pub fn read_routes(service: Arc<S>, base_path: &str) -> axum::Router<()>
1437 where
1438 S: Clone,
1439 {
1440 if let Some(table) = service.table_name() {
1443 backbone_orm::secret_registry::register(table, E::secret_fields());
1444 }
1445 use axum::{
1446 extract::{Path, Query},
1447 routing::get,
1448 Extension, Router,
1449 };
1450
1451 let handler = Arc::new(Self::new(service));
1452
1453 Router::new()
1454 .route(base_path, get({
1456 let h = handler.clone();
1457 move |query: Query<ListQueryParams>, access: Option<Extension<AccessScope>>| async move {
1458 Self::list_handler(h, query, access).await
1459 }
1460 }))
1461 .route(&format!("{}/trash", base_path), get({
1463 let h = handler.clone();
1464 move |query: Query<ListQueryParams>, access: Option<Extension<AccessScope>>| async move {
1465 Self::list_deleted_handler(h, query, access).await
1466 }
1467 }))
1468 .route(&format!("{}/:id", base_path), get({
1470 let h = handler.clone();
1471 move |path: Path<String>, query: Query<ListQueryParams>, access: Option<Extension<AccessScope>>| async move {
1472 Self::get_handler(h, path, query, access).await
1473 }
1474 }))
1475 .route(&format!("{}/:id/deleted", base_path), get({
1477 let h = handler.clone();
1478 move |path: Path<String>, query: Query<ListQueryParams>, access: Option<Extension<AccessScope>>| async move {
1479 Self::get_deleted_handler(h, path, query, access).await
1480 }
1481 }))
1482 .route(&format!("{}/:id/history", base_path), get({
1485 let h = handler.clone();
1486 move |path: axum::extract::Path<String>,
1487 query: axum::extract::Query<ListQueryParams>,
1488 provider: Option<axum::Extension<std::sync::Arc<dyn HistoryProvider>>>| async move {
1489 Self::history_handler(h, path, query, provider).await
1490 }
1491 }))
1492 .route(&format!("{}/aggregate", base_path), get({
1494 let h = handler.clone();
1495 move |query: axum::extract::Query<ListQueryParams>| async move {
1496 Self::aggregate_handler(h, query).await
1497 }
1498 }))
1499 .route(&format!("{}/count", base_path), get({
1500 let h = handler.clone();
1501 move |query: axum::extract::Query<ListQueryParams>| async move {
1502 Self::count_active_handler(h, query).await
1503 }
1504 }))
1505 .route(&format!("{}/trash/count", base_path), get({
1507 let h = handler.clone();
1508 move || async move {
1509 Self::count_deleted_handler(h).await
1510 }
1511 }))
1512 }
1513
1514 pub fn write_routes(service: Arc<S>, base_path: &str) -> axum::Router<()>
1531 where
1532 S: Clone,
1533 {
1534 if let Some(table) = service.table_name() {
1537 backbone_orm::secret_registry::register(table, E::secret_fields());
1538 }
1539 use axum::{
1540 extract::Path,
1541 routing::{delete, patch, post, put},
1542 Router,
1543 };
1544
1545 let handler = Arc::new(Self::new(service));
1546
1547 Router::new()
1548 .route(base_path, post({
1550 let h = handler.clone();
1551 move |body: StrictJsonOrForm<C>| async move {
1552 Self::create_handler(h, body).await
1553 }
1554 }))
1555 .route(&format!("{}/bulk", base_path), post({
1557 let h = handler.clone();
1558 move |body: StrictJsonOrForm<Vec<C>>| async move {
1559 Self::bulk_create_handler(h, body).await
1560 }
1561 }))
1562 .route(&format!("{}/upsert", base_path), post({
1564 let h = handler.clone();
1565 move |body: StrictJsonOrForm<C>| async move {
1566 Self::upsert_handler(h, body).await
1567 }
1568 }))
1569 .route(&format!("{}/delete/bulk", base_path), post({
1571 let h = handler.clone();
1572 move |body: JsonOrForm<BatchIdsRequest>| async move {
1573 Self::bulk_delete_handler(h, body).await
1574 }
1575 }))
1576 .route(&format!("{}/restore/bulk", base_path), post({
1578 let h = handler.clone();
1579 move |body: JsonOrForm<BatchIdsRequest>| async move {
1580 Self::bulk_restore_handler(h, body).await
1581 }
1582 }))
1583 .route(&format!("{}/restore/all", base_path), post({
1585 let h = handler.clone();
1586 move || async move {
1587 Self::restore_all_handler(h).await
1588 }
1589 }))
1590 .route(&format!("{}/trash/bulk", base_path), delete({
1593 let h = handler.clone();
1594 move |body: JsonOrForm<BatchIdsRequest>| async move {
1595 Self::bulk_permanent_delete_handler(h, body).await
1596 }
1597 }))
1598 .route(&format!("{}/bulk", base_path), put({
1600 let h = handler.clone();
1601 move |body: JsonOrForm<Vec<serde_json::Map<String, serde_json::Value>>>| async move {
1602 Self::bulk_update_handler(h, body).await
1603 }
1604 }))
1605 .route(&format!("{}/bulk", base_path), patch({
1607 let h = handler.clone();
1608 move |body: JsonOrForm<BulkPatchRequest>| async move {
1609 Self::bulk_patch_handler(h, body).await
1610 }
1611 }))
1612 .route(&format!("{}/bulk/preview", base_path), post({
1614 let h = handler.clone();
1615 move |body: JsonOrForm<BulkPatchRequest>| async move {
1616 Self::bulk_patch_preview_handler(h, body).await
1617 }
1618 }))
1619 .route(&format!("{}/empty", base_path), delete({
1621 let h = handler.clone();
1622 move || async move {
1623 Self::empty_trash_handler(h).await
1624 }
1625 }))
1626 .route(&format!("{}/trash/:id", base_path), delete({
1628 let h = handler.clone();
1629 move |path: Path<String>| async move {
1630 Self::permanent_delete_handler(h, path).await
1631 }
1632 }))
1633 .route(&format!("{}/:id", base_path), put({
1635 let h = handler.clone();
1636 move |path: Path<String>, body: StrictJsonOrForm<U>| async move {
1637 Self::update_handler(h, path, body).await
1638 }
1639 }))
1640 .route(&format!("{}/:id", base_path), patch({
1642 let h = handler.clone();
1643 move |path: Path<String>, body: JsonOrForm<HashMap<String, serde_json::Value>>| async move {
1644 Self::partial_update_handler(h, path, body).await
1645 }
1646 }))
1647 .route(&format!("{}/:id", base_path), delete({
1649 let h = handler.clone();
1650 move |path: Path<String>| async move {
1651 Self::delete_handler(h, path).await
1652 }
1653 }))
1654 .route(&format!("{}/:id/restore", base_path), post({
1656 let h = handler.clone();
1657 move |path: Path<String>| async move {
1658 Self::restore_handler(h, path).await
1659 }
1660 }))
1661 }
1662
1663 fn failure<T>(err: &S::Error, fallback: axum::http::StatusCode) -> (axum::http::StatusCode, axum::Json<ApiResponse<T>>) {
1671 let violations = S::violations_of(err);
1672 let status = S::status_of(err).unwrap_or(if violations.is_some() {
1673 axum::http::StatusCode::UNPROCESSABLE_ENTITY
1674 } else {
1675 fallback
1676 });
1677 error_at(status, violations, err)
1678 }
1679
1680 fn list_failure(err: &S::Error) -> (axum::http::StatusCode, axum::Json<PaginatedApiResponse<serde_json::Value>>) {
1683 use axum::{http::StatusCode, Json};
1684 let msg = err.to_string();
1685 if is_bad_query_error(&msg) {
1686 return (
1687 StatusCode::BAD_REQUEST,
1688 Json(PaginatedApiResponse::error(format!("Invalid query parameter or filter: {msg}"))),
1689 );
1690 }
1691 let (status, Json(body)) = Self::failure::<()>(err, StatusCode::INTERNAL_SERVER_ERROR);
1692 (status, Json(PaginatedApiResponse::error(body.error.unwrap_or_default())))
1693 }
1694
1695 async fn list_handler(
1696 handler: Arc<Self>,
1697 axum::extract::Query(params): axum::extract::Query<ListQueryParams>,
1698 access: Option<axum::Extension<AccessScope>>,
1699 ) -> impl axum::response::IntoResponse {
1700 use axum::{http::StatusCode, Json};
1701
1702 if let Some(err) = pagination_depth_error(params.page, params.limit) {
1703 return (StatusCode::BAD_REQUEST, Json(PaginatedApiResponse::<serde_json::Value>::error(err)));
1704 }
1705 if let Some(field) = named_secret(¶ms, E::secret_fields()) {
1706 return (
1707 StatusCode::BAD_REQUEST,
1708 Json(PaginatedApiResponse::<serde_json::Value>::error(secret_named_message(&field))),
1709 );
1710 }
1711
1712 let fields = sparse_fields(¶ms.filters);
1715 let includes = include_relations(¶ms.filters);
1716 let scope = access.map(|axum::Extension(s)| s);
1717
1718 let filters = repository_filters(¶ms);
1719
1720 match handler
1721 .service
1722 .list_with_info(params.page, params.limit, filters)
1723 .await
1724 {
1725 Ok((entities, info)) => {
1726 let mut rows: Vec<serde_json::Value> = entities
1729 .into_iter()
1730 .map(|e| {
1731 secure::<E>(to_response_value(R::from(e)), scope.as_ref())
1732 })
1733 .collect();
1734 expand_includes::<S, E, C, U>(&*handler.service, &mut rows, &includes).await;
1735 let items: Vec<serde_json::Value> =
1736 rows.into_iter().map(|r| project_sparse(r, &fields)).collect();
1737 let response = PaginatedApiResponse::ok_with_info(items, &info);
1738 (StatusCode::OK, Json(response))
1739 }
1740 Err(e) => Self::list_failure(&e),
1741 }
1742 }
1743
1744 async fn create_handler(
1745 handler: Arc<Self>,
1746 StrictJsonOrForm(dto): StrictJsonOrForm<C>,
1747 ) -> impl axum::response::IntoResponse {
1748 use axum::{http::StatusCode, Json};
1749
1750 match handler.service.create(dto).await {
1751 Ok(entity) => {
1752 let response = secure::<E>(to_response_value(R::from(entity)), None);
1753 (StatusCode::CREATED, Json(ApiResponse::ok(response)))
1754 }
1755 Err(e) => Self::failure(&e, StatusCode::BAD_REQUEST),
1756 }
1757 }
1758
1759 async fn get_handler(
1760 handler: Arc<Self>,
1761 axum::extract::Path(id): axum::extract::Path<String>,
1762 axum::extract::Query(params): axum::extract::Query<ListQueryParams>,
1763 access: Option<axum::Extension<AccessScope>>,
1764 ) -> impl axum::response::IntoResponse {
1765 use axum::{http::StatusCode, Json};
1766
1767 if let Some(refused) = invalid_id::<serde_json::Value>(&id, "id") {
1768 return refused;
1769 }
1770
1771 let fields = sparse_fields(¶ms.filters);
1772 let includes = include_relations(¶ms.filters);
1773 let scope = access.map(|axum::Extension(s)| s);
1774
1775 match handler.service.get_by_id(&id).await {
1776 Ok(Some(entity)) => {
1777 let secured = secure::<E>(to_response_value(R::from(entity)), scope.as_ref());
1778 let mut rows = [secured];
1779 expand_includes::<S, E, C, U>(&*handler.service, &mut rows, &includes).await;
1780 let [secured] = rows;
1781 let value = project_sparse(secured, &fields);
1782 (StatusCode::OK, Json(ApiResponse::ok(value)))
1783 }
1784 Ok(None) => {
1785 (StatusCode::NOT_FOUND, Json(ApiResponse::<serde_json::Value>::not_found(S::entity_name(), &id)))
1786 }
1787 Err(e) => {
1788 Self::failure(&e, StatusCode::INTERNAL_SERVER_ERROR)
1789 }
1790 }
1791 }
1792
1793 async fn update_handler(
1794 handler: Arc<Self>,
1795 axum::extract::Path(id): axum::extract::Path<String>,
1796 StrictJsonOrForm(dto): StrictJsonOrForm<U>,
1797 ) -> impl axum::response::IntoResponse {
1798 use axum::{http::StatusCode, Json};
1799
1800 if let Some(refused) = invalid_id::<serde_json::Value>(&id, "id") {
1801 return refused;
1802 }
1803
1804 match handler.service.update(&id, dto).await {
1805 Ok(Some(entity)) => {
1806 let response = secure::<E>(to_response_value(R::from(entity)), None);
1807 (StatusCode::OK, Json(ApiResponse::ok(response)))
1808 }
1809 Ok(None) => {
1810 (StatusCode::NOT_FOUND, Json(ApiResponse::<serde_json::Value>::not_found(S::entity_name(), &id)))
1811 }
1812 Err(e) => {
1813 Self::failure(&e, StatusCode::BAD_REQUEST)
1814 }
1815 }
1816 }
1817
1818 async fn partial_update_handler(
1819 handler: Arc<Self>,
1820 axum::extract::Path(id): axum::extract::Path<String>,
1821 JsonOrForm(fields): JsonOrForm<HashMap<String, serde_json::Value>>,
1822 ) -> impl axum::response::IntoResponse {
1823 use axum::{http::StatusCode, Json};
1824
1825 if let Some(refused) = invalid_id::<serde_json::Value>(&id, "id") {
1826 return refused;
1827 }
1828
1829 let fields: HashMap<String, serde_json::Value> = fields
1840 .into_iter()
1841 .map(|(k, v)| (camel_to_snake_case(&k), v))
1842 .collect();
1843
1844 match handler.service.partial_update(&id, fields).await {
1845 Ok(Some(entity)) => {
1846 let response = secure::<E>(to_response_value(R::from(entity)), None);
1847 (StatusCode::OK, Json(ApiResponse::ok(response)))
1848 }
1849 Ok(None) => {
1850 (StatusCode::NOT_FOUND, Json(ApiResponse::<serde_json::Value>::not_found(S::entity_name(), &id)))
1851 }
1852 Err(e) => {
1853 Self::failure(&e, StatusCode::BAD_REQUEST)
1854 }
1855 }
1856 }
1857
1858 async fn delete_handler(
1859 handler: Arc<Self>,
1860 axum::extract::Path(id): axum::extract::Path<String>,
1861 ) -> impl axum::response::IntoResponse {
1862 use axum::{http::StatusCode, Json};
1863
1864 if let Some(refused) = invalid_id::<()>(&id, "id") {
1865 return refused;
1866 }
1867
1868 match handler.service.soft_delete(&id).await {
1869 Ok(true) => {
1870 (StatusCode::OK, Json(ApiResponse::<()>::success_with_message((), "Entity deleted successfully")))
1873 }
1874 Ok(false) => {
1875 (StatusCode::NOT_FOUND, Json(ApiResponse::<()>::not_found(S::entity_name(), &id)))
1876 }
1877 Err(e) => {
1878 Self::failure(&e, StatusCode::INTERNAL_SERVER_ERROR)
1879 }
1880 }
1881 }
1882
1883 async fn bulk_create_handler(
1884 handler: Arc<Self>,
1885 StrictJsonOrForm(items): StrictJsonOrForm<Vec<C>>,
1886 ) -> impl axum::response::IntoResponse {
1887 use axum::{http::StatusCode, Json};
1888
1889 if let Some(err) = batch_size_error(items.len()) {
1890 return (StatusCode::BAD_REQUEST, Json(ApiResponse::<BulkResponse<serde_json::Value>>::error(err)));
1891 }
1892 match handler.service.bulk_create(items).await {
1893 Ok(entities) => {
1894 let result_items: Vec<serde_json::Value> = entities
1895 .into_iter()
1896 .map(|e| secure::<E>(to_response_value(R::from(e)), None))
1897 .collect();
1898 let total = result_items.len();
1899 let response = BulkResponse {
1900 items: result_items,
1901 total,
1902 failed: 0,
1903 errors: vec![],
1904 };
1905 (StatusCode::CREATED, Json(ApiResponse::ok(response)))
1906 }
1907 Err(e) => {
1908 Self::failure(&e, StatusCode::BAD_REQUEST)
1909 }
1910 }
1911 }
1912
1913 async fn bulk_delete_handler(
1916 handler: Arc<Self>,
1917 JsonOrForm(req): JsonOrForm<BatchIdsRequest>,
1918 ) -> impl axum::response::IntoResponse {
1919 use axum::{http::StatusCode, Json};
1920
1921 if let Some(err) = batch_size_error(req.ids.len()) {
1922 return (StatusCode::BAD_REQUEST, Json(ApiResponse::<serde_json::Value>::error(err)));
1923 }
1924 if let Some(refused) = invalid_ids::<serde_json::Value>(req.ids.iter().map(String::as_str), |i| format!("ids/{i}")) {
1925 return refused;
1926 }
1927 match handler.service.bulk_soft_delete(req.ids).await {
1928 Ok(count) => (StatusCode::OK, Json(ApiResponse::success_with_message(
1929 serde_json::json!({ "soft_deleted": count }),
1930 format!("Soft-deleted {count} item(s)"),
1931 ))),
1932 Err(e) => Self::failure(&e, StatusCode::BAD_REQUEST),
1933 }
1934 }
1935
1936 async fn bulk_restore_handler(
1937 handler: Arc<Self>,
1938 JsonOrForm(req): JsonOrForm<BatchIdsRequest>,
1939 ) -> impl axum::response::IntoResponse {
1940 use axum::{http::StatusCode, Json};
1941
1942 if let Some(err) = batch_size_error(req.ids.len()) {
1943 return (StatusCode::BAD_REQUEST, Json(ApiResponse::<BulkResponse<serde_json::Value>>::error(err)));
1944 }
1945 if let Some(refused) = invalid_ids::<BulkResponse<serde_json::Value>>(req.ids.iter().map(String::as_str), |i| format!("ids/{i}")) {
1946 return refused;
1947 }
1948 match handler.service.bulk_restore(req.ids).await {
1949 Ok(entities) => {
1950 let items: Vec<serde_json::Value> = entities
1951 .into_iter()
1952 .map(|e| secure::<E>(to_response_value(R::from(e)), None))
1953 .collect();
1954 let total = items.len();
1955 (StatusCode::OK, Json(ApiResponse::ok(BulkResponse { items, total, failed: 0, errors: vec![] })))
1956 }
1957 Err(e) => Self::failure(&e, StatusCode::BAD_REQUEST),
1958 }
1959 }
1960
1961 async fn restore_all_handler(
1962 handler: Arc<Self>,
1963 ) -> impl axum::response::IntoResponse {
1964 use axum::{http::StatusCode, Json};
1965
1966 match handler.service.restore_all().await {
1967 Ok(count) => (StatusCode::OK, Json(ApiResponse::success_with_message(
1968 serde_json::json!({ "restored": count }),
1969 format!("Restored {count} item(s) from trash"),
1970 ))),
1971 Err(e) => Self::failure(&e, StatusCode::INTERNAL_SERVER_ERROR),
1972 }
1973 }
1974
1975 async fn bulk_permanent_delete_handler(
1976 handler: Arc<Self>,
1977 JsonOrForm(req): JsonOrForm<BatchIdsRequest>,
1978 ) -> impl axum::response::IntoResponse {
1979 use axum::{http::StatusCode, Json};
1980
1981 if let Some(err) = batch_size_error(req.ids.len()) {
1982 return (StatusCode::BAD_REQUEST, Json(ApiResponse::<serde_json::Value>::error(err)));
1983 }
1984 if let Some(refused) = invalid_ids::<serde_json::Value>(req.ids.iter().map(String::as_str), |i| format!("ids/{i}")) {
1985 return refused;
1986 }
1987 match handler.service.bulk_permanent_delete(req.ids).await {
1988 Ok(count) => (StatusCode::OK, Json(ApiResponse::success_with_message(
1989 serde_json::json!({ "permanently_deleted": count }),
1990 format!("Permanently deleted {count} item(s)"),
1991 ))),
1992 Err(e) => Self::failure(&e, StatusCode::BAD_REQUEST),
1993 }
1994 }
1995
1996 async fn bulk_update_handler(
2000 handler: Arc<Self>,
2001 JsonOrForm(raw): JsonOrForm<Vec<serde_json::Map<String, serde_json::Value>>>,
2002 ) -> axum::response::Response {
2003 use axum::{http::StatusCode, response::IntoResponse, Json};
2004
2005 if let Some(err) = batch_size_error(raw.len()) {
2006 return (StatusCode::BAD_REQUEST, Json(ApiResponse::<BulkResponse<serde_json::Value>>::error(err)))
2007 .into_response();
2008 }
2009 let mut items: Vec<(String, U)> = Vec::with_capacity(raw.len());
2010 for (index, mut fields) in raw.into_iter().enumerate() {
2011 let id = match fields.remove("id") {
2012 Some(serde_json::Value::String(id)) => id,
2013 _ => {
2014 let violation = crate::violation::Violation::new(
2015 crate::violation::batch_item_path(index, "id"),
2016 "required",
2017 "id is required",
2018 );
2019 return (
2020 StatusCode::UNPROCESSABLE_ENTITY,
2021 Json(ApiResponse::<()>::rejected(format!("item {index}: id is required"), vec![violation])),
2022 )
2023 .into_response();
2024 }
2025 };
2026 if let Some(refused) = invalid_id::<()>(&id, &crate::violation::batch_item_path(index, "id")) {
2027 return refused.into_response();
2028 }
2029 let bytes = match serde_json::to_vec(&fields) {
2030 Ok(bytes) => bytes,
2031 Err(e) => return error_at::<()>(StatusCode::INTERNAL_SERVER_ERROR, None, &e).into_response(),
2032 };
2033 match crate::extractors::decode_json_or_refusal::<U>(&bytes, true) {
2034 Ok(data) => items.push((id, data)),
2035 Err(refusal) => return refusal.at_item(index).into_response(),
2036 }
2037 }
2038 match handler.service.bulk_update(items).await {
2039 Ok(entities) => {
2040 let items: Vec<serde_json::Value> = entities
2041 .into_iter()
2042 .map(|e| secure::<E>(to_response_value(R::from(e)), None))
2043 .collect();
2044 let total = items.len();
2045 (StatusCode::OK, Json(ApiResponse::ok(BulkResponse { items, total, failed: 0, errors: vec![] })))
2046 .into_response()
2047 }
2048 Err(e) => Self::failure::<()>(&e, StatusCode::BAD_REQUEST).into_response(),
2049 }
2050 }
2051
2052 async fn bulk_patch_handler(
2053 handler: Arc<Self>,
2054 JsonOrForm(req): JsonOrForm<BulkPatchRequest>,
2055 ) -> impl axum::response::IntoResponse {
2056 use axum::{http::StatusCode, Json};
2057
2058 let items = req.into_items();
2059 if let Some(err) = batch_size_error(items.len()) {
2060 return (StatusCode::BAD_REQUEST, Json(ApiResponse::<BulkResponse<serde_json::Value>>::error(err)));
2061 }
2062 if let Some(refused) = invalid_ids::<BulkResponse<serde_json::Value>>(items.iter().map(|(id, _)| id.as_str()), |i| format!("{i}/id")) {
2063 return refused;
2064 }
2065 let items: Vec<(String, HashMap<String, serde_json::Value>)> = items
2067 .into_iter()
2068 .map(|(id, fields)| {
2069 let fields = fields
2070 .into_iter()
2071 .map(|(k, v)| (camel_to_snake_case(&k), v))
2072 .collect();
2073 (id, fields)
2074 })
2075 .collect();
2076 match handler.service.bulk_partial_update(items).await {
2077 Ok(entities) => {
2078 let items: Vec<serde_json::Value> = entities
2079 .into_iter()
2080 .map(|e| secure::<E>(to_response_value(R::from(e)), None))
2081 .collect();
2082 let total = items.len();
2083 (StatusCode::OK, Json(ApiResponse::ok(BulkResponse { items, total, failed: 0, errors: vec![] })))
2084 }
2085 Err(e) => Self::failure(&e, StatusCode::BAD_REQUEST),
2086 }
2087 }
2088
2089 async fn bulk_patch_preview_handler(
2095 handler: Arc<Self>,
2096 JsonOrForm(req): JsonOrForm<BulkPatchRequest>,
2097 ) -> impl axum::response::IntoResponse {
2098 use axum::{http::StatusCode, Json};
2099
2100 let items = req.into_items();
2101 if let Some(err) = batch_size_error(items.len()) {
2102 return (StatusCode::BAD_REQUEST, Json(ApiResponse::<BulkPreview>::error(err)));
2103 }
2104 if let Some(refused) = invalid_ids::<BulkPreview>(items.iter().map(|(id, _)| id.as_str()), |i| format!("{i}/id")) {
2105 return refused;
2106 }
2107 let items: Vec<(String, HashMap<String, serde_json::Value>)> = items
2108 .into_iter()
2109 .map(|(id, fields)| {
2110 let fields = fields
2111 .into_iter()
2112 .map(|(k, v)| (camel_to_snake_case(&k), v))
2113 .collect();
2114 (id, fields)
2115 })
2116 .collect();
2117 let patched: HashMap<String, Vec<String>> = items
2118 .iter()
2119 .map(|(id, f)| (id.clone(), f.keys().cloned().collect()))
2120 .collect();
2121 match handler.service.preview_bulk_partial_update(items).await {
2122 Ok(Some(rows)) => {
2123 let rows = rows
2124 .into_iter()
2125 .map(|row| {
2126 let outcome = match row.outcome {
2127 Ok((before, after)) => Ok((
2128 secure::<E>(to_response_value(R::from(before)), None),
2129 secure::<E>(to_response_value(R::from(after)), None),
2130 )),
2131 Err(e) => Err((
2132 S::violations_of(&e)
2133 .and_then(|v| v.into_iter().next().map(|v| v.code))
2134 .unwrap_or_else(|| "refused".to_string()),
2135 e.to_string(),
2136 )),
2137 };
2138 (row.id, outcome)
2139 })
2140 .collect();
2141 (StatusCode::OK, Json(ApiResponse::ok(preview_answer(rows, &patched))))
2142 }
2143 Ok(None) => (
2144 StatusCode::NOT_IMPLEMENTED,
2145 Json(ApiResponse::error("this resource has no preview of a bulk change".to_string())),
2146 ),
2147 Err(e) => Self::failure(&e, StatusCode::BAD_REQUEST),
2148 }
2149 }
2150
2151 async fn upsert_handler(
2152 handler: Arc<Self>,
2153 StrictJsonOrForm(dto): StrictJsonOrForm<C>,
2154 ) -> impl axum::response::IntoResponse {
2155 use axum::{http::StatusCode, Json};
2156
2157 if !S::supports_upsert() {
2160 return (
2161 StatusCode::NOT_IMPLEMENTED,
2162 Json(ApiResponse::<serde_json::Value>::error(crate::service::UPSERT_UNSUPPORTED)),
2163 );
2164 }
2165 match handler.service.upsert(dto).await {
2166 Ok(entity) => {
2167 let response = secure::<E>(to_response_value(R::from(entity)), None);
2168 (StatusCode::OK, Json(ApiResponse::ok(response)))
2169 }
2170 Err(e) => {
2171 Self::failure(&e, StatusCode::BAD_REQUEST)
2172 }
2173 }
2174 }
2175
2176 async fn list_deleted_handler(
2177 handler: Arc<Self>,
2178 axum::extract::Query(params): axum::extract::Query<ListQueryParams>,
2179 access: Option<axum::Extension<AccessScope>>,
2180 ) -> impl axum::response::IntoResponse {
2181 use axum::{http::StatusCode, Json};
2182
2183 if let Some(err) = pagination_depth_error(params.page, params.limit) {
2184 return (StatusCode::BAD_REQUEST, Json(PaginatedApiResponse::<serde_json::Value>::error(err)));
2185 }
2186 if let Some(field) = named_secret(¶ms, E::secret_fields()) {
2187 return (
2188 StatusCode::BAD_REQUEST,
2189 Json(PaginatedApiResponse::<serde_json::Value>::error(secret_named_message(&field))),
2190 );
2191 }
2192
2193 let fields = sparse_fields(¶ms.filters);
2194 let includes = include_relations(¶ms.filters);
2195 let scope = access.map(|axum::Extension(s)| s);
2196
2197 let filters = repository_filters(¶ms);
2198 match handler.service.list_deleted_filtered(params.page, params.limit, filters).await {
2199 Ok((entities, total)) => {
2200 let mut rows: Vec<serde_json::Value> = entities
2204 .into_iter()
2205 .map(|e| {
2206 secure::<E>(to_response_value(R::from(e)), scope.as_ref())
2207 })
2208 .collect();
2209 expand_includes::<S, E, C, U>(&*handler.service, &mut rows, &includes).await;
2210 let items: Vec<serde_json::Value> =
2211 rows.into_iter().map(|r| project_sparse(r, &fields)).collect();
2212 let response = PaginatedApiResponse::ok(items, total, params.page, params.limit);
2213 (StatusCode::OK, Json(response))
2214 }
2215 Err(e) => Self::list_failure(&e),
2216 }
2217 }
2218
2219 async fn restore_handler(
2220 handler: Arc<Self>,
2221 axum::extract::Path(id): axum::extract::Path<String>,
2222 ) -> impl axum::response::IntoResponse {
2223 use axum::{http::StatusCode, Json};
2224
2225 if let Some(refused) = invalid_id::<serde_json::Value>(&id, "id") {
2226 return refused;
2227 }
2228
2229 match handler.service.restore(&id).await {
2230 Ok(Some(entity)) => {
2231 let response = secure::<E>(to_response_value(R::from(entity)), None);
2232 (StatusCode::OK, Json(ApiResponse::success_with_message(response, "Entity restored successfully")))
2233 }
2234 Ok(None) => {
2235 (StatusCode::NOT_FOUND, Json(ApiResponse::<serde_json::Value>::not_found(S::entity_name(), &id)))
2236 }
2237 Err(e) => {
2238 Self::failure(&e, StatusCode::BAD_REQUEST)
2239 }
2240 }
2241 }
2242
2243 async fn empty_trash_handler(
2244 handler: Arc<Self>,
2245 ) -> impl axum::response::IntoResponse {
2246 use axum::{http::StatusCode, Json};
2247
2248 match handler.service.empty_trash().await {
2249 Ok(count) => {
2250 (StatusCode::OK, Json(ApiResponse::success_with_message(
2251 serde_json::json!({ "deleted_count": count }),
2252 format!("Successfully deleted {} items from trash", count)
2253 )))
2254 }
2255 Err(e) => {
2256 Self::failure(&e, StatusCode::INTERNAL_SERVER_ERROR)
2257 }
2258 }
2259 }
2260
2261 async fn get_deleted_handler(
2263 handler: Arc<Self>,
2264 axum::extract::Path(id): axum::extract::Path<String>,
2265 axum::extract::Query(params): axum::extract::Query<ListQueryParams>,
2266 access: Option<axum::Extension<AccessScope>>,
2267 ) -> impl axum::response::IntoResponse {
2268 use axum::{http::StatusCode, Json};
2269
2270 if let Some(refused) = invalid_id::<serde_json::Value>(&id, "id") {
2271 return refused;
2272 }
2273
2274 let fields = sparse_fields(¶ms.filters);
2275 let scope = access.map(|axum::Extension(s)| s);
2276
2277 match handler.service.get_deleted_by_id(&id).await {
2278 Ok(Some(entity)) => {
2279 let secured = secure::<E>(to_response_value(R::from(entity)), scope.as_ref());
2280 let value = project_sparse(secured, &fields);
2281 (StatusCode::OK, Json(ApiResponse::ok(value)))
2282 }
2283 Ok(None) => {
2284 (StatusCode::NOT_FOUND, Json(ApiResponse::<serde_json::Value>::not_found(&format!("Deleted {}", S::entity_name()), &id)))
2285 }
2286 Err(e) => {
2287 Self::failure(&e, StatusCode::INTERNAL_SERVER_ERROR)
2288 }
2289 }
2290 }
2291
2292 async fn permanent_delete_handler(
2294 handler: Arc<Self>,
2295 axum::extract::Path(id): axum::extract::Path<String>,
2296 ) -> axum::response::Response {
2297 use axum::{http::StatusCode, Json, response::IntoResponse};
2298
2299 if let Some(refused) = invalid_id::<R>(&id, "id") {
2300 return refused.into_response();
2301 }
2302
2303 match handler.service.get_deleted_by_id(&id).await {
2305 Ok(Some(_)) => {
2306 match handler.service.permanent_delete(&id).await {
2308 Ok(true) => {
2309 StatusCode::NO_CONTENT.into_response()
2311 }
2312 Ok(false) => {
2313 (StatusCode::NOT_FOUND, Json(ApiResponse::<R>::error(
2314 format!("Failed to permanently delete {}", S::entity_name())
2315 ))).into_response()
2316 }
2317 Err(e) => {
2318 Self::failure::<R>(&e, StatusCode::INTERNAL_SERVER_ERROR).into_response()
2319 }
2320 }
2321 }
2322 Ok(None) => {
2323 (StatusCode::NOT_FOUND, Json(ApiResponse::<R>::not_found(
2324 &format!("{} in trash", S::entity_name()), &id
2325 ))).into_response()
2326 }
2327 Err(e) => {
2328 Self::failure::<R>(&e, StatusCode::INTERNAL_SERVER_ERROR).into_response()
2329 }
2330 }
2331 }
2332
2333 async fn count_active_handler(
2335 handler: Arc<Self>,
2336 axum::extract::Query(params): axum::extract::Query<ListQueryParams>,
2337 ) -> impl axum::response::IntoResponse {
2338 use axum::{http::StatusCode, Json};
2339
2340 if let Some(field) = named_secret(¶ms, E::secret_fields()) {
2344 return (
2345 StatusCode::BAD_REQUEST,
2346 Json(ApiResponse::<serde_json::Value>::error(secret_named_message(&field))),
2347 );
2348 }
2349 match handler.service.count_active_filtered(repository_filters(¶ms)).await {
2350 Ok(count) => {
2351 (StatusCode::OK, Json(ApiResponse::ok(serde_json::json!({ "count": count }))))
2352 }
2353 Err(e) => {
2354 if is_bad_query_error(&e.to_string()) {
2357 (StatusCode::BAD_REQUEST, Json(ApiResponse::<serde_json::Value>::error(e.to_string())))
2358 } else {
2359 Self::failure(&e, StatusCode::INTERNAL_SERVER_ERROR)
2360 }
2361 }
2362 }
2363 }
2364
2365 async fn aggregate_handler(
2370 handler: Arc<Self>,
2371 axum::extract::Query(params): axum::extract::Query<ListQueryParams>,
2372 ) -> impl axum::response::IntoResponse {
2373 use axum::{http::StatusCode, Json};
2374
2375 if let Some(field) = named_secret(¶ms, E::secret_fields()) {
2376 return (
2377 StatusCode::BAD_REQUEST,
2378 Json(ApiResponse::<serde_json::Value>::error(secret_named_message(&field))),
2379 );
2380 }
2381 let spec = aggregate_spec(¶ms);
2382 match handler.service.aggregate(&spec, aggregate_filters(¶ms)).await {
2383 Ok(result) => {
2384 let render = |g: &backbone_orm::repository::AggregateGroup| {
2385 let mut out = serde_json::Map::new();
2389 out.insert("key".into(), match &g.key {
2390 Some(k) => serde_json::Value::String(k.clone()),
2391 None => serde_json::Value::Null,
2392 });
2393 if g.label.is_some() {
2394 out.insert(
2395 "label".into(),
2396 serde_json::Value::String(g.label.clone().unwrap()),
2397 );
2398 }
2399 out.insert("count".into(), serde_json::json!(g.count));
2400 for (compound, value) in &g.values {
2401 let Some((func, field)) = compound.split_once(':') else { continue };
2402 let slot = out
2403 .entry(func.to_string())
2404 .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
2405 if let Some(obj) = slot.as_object_mut() {
2406 obj.insert(field.to_string(), match value {
2407 Some(v) => serde_json::Value::String(v.clone()),
2411 None => serde_json::Value::Null,
2412 });
2413 }
2414 }
2415 serde_json::Value::Object(out)
2416 };
2417
2418 let body = serde_json::json!({
2419 "groups": result.groups.iter().map(render).collect::<Vec<_>>(),
2420 "total": render(&result.total),
2421 "truncated": result.truncated,
2424 });
2425 (StatusCode::OK, Json(ApiResponse::ok(body)))
2426 }
2427 Err(e) => {
2428 if is_bad_query_error(&e.to_string()) {
2429 (StatusCode::BAD_REQUEST, Json(ApiResponse::<serde_json::Value>::error(e.to_string())))
2430 } else {
2431 Self::failure(&e, StatusCode::INTERNAL_SERVER_ERROR)
2432 }
2433 }
2434 }
2435 }
2436
2437 async fn history_handler(
2448 handler: Arc<Self>,
2449 axum::extract::Path(id): axum::extract::Path<String>,
2450 axum::extract::Query(params): axum::extract::Query<ListQueryParams>,
2451 provider: Option<axum::Extension<std::sync::Arc<dyn HistoryProvider>>>,
2452 ) -> impl axum::response::IntoResponse {
2453 use axum::{http::StatusCode, Json};
2454
2455 if let Some(refused) = invalid_id::<serde_json::Value>(&id, "id") {
2456 return refused;
2457 }
2458
2459 let Some(axum::Extension(provider)) = provider else {
2460 return (
2461 StatusCode::NOT_IMPLEMENTED,
2462 Json(ApiResponse::<serde_json::Value>::error(
2463 "history is not configured for this service".to_string(),
2464 )),
2465 );
2466 };
2467
2468 let Some(table) = handler.service.table_name().map(|t| t.to_string()) else {
2472 return (
2473 StatusCode::NOT_IMPLEMENTED,
2474 Json(ApiResponse::<serde_json::Value>::error(
2475 "this entity cannot name its table, so its history cannot be keyed".to_string(),
2476 )),
2477 );
2478 };
2479
2480 let limit = params.limit.clamp(1, 200);
2481 let offset = params.page.saturating_sub(1) * limit;
2482
2483 match provider.history(&table, &id, limit, offset).await {
2484 Ok(Some(entries)) => (
2485 StatusCode::OK,
2486 Json(ApiResponse::ok(serde_json::json!({
2487 "audited": true,
2488 "entries": entries,
2489 }))),
2490 ),
2491 Ok(None) => (
2492 StatusCode::OK,
2493 Json(ApiResponse::ok(serde_json::json!({
2494 "audited": false,
2497 "entries": serde_json::Value::Null,
2498 }))),
2499 ),
2500 Err(e) => error_at(StatusCode::INTERNAL_SERVER_ERROR, None, &e),
2501 }
2502 }
2503
2504 async fn count_deleted_handler(
2506 handler: Arc<Self>,
2507 ) -> impl axum::response::IntoResponse {
2508 use axum::{http::StatusCode, Json};
2509
2510 match handler.service.count_deleted().await {
2511 Ok(count) => {
2512 (StatusCode::OK, Json(ApiResponse::ok(serde_json::json!({ "count": count }))))
2513 }
2514 Err(e) => {
2515 Self::failure(&e, StatusCode::INTERNAL_SERVER_ERROR)
2516 }
2517 }
2518 }
2519}
2520
2521pub trait BackboneHttpHandler<T>: Send + Sync {
2546 fn list(&self, request: ListRequest) -> anyhow::Result<ApiResponse<Vec<T>>>;
2548
2549 fn create(&self, request: T) -> anyhow::Result<ApiResponse<T>>;
2551
2552 fn get_by_id(&self, id: &uuid::Uuid) -> anyhow::Result<ApiResponse<T>>;
2554
2555 fn update(&self, id: &uuid::Uuid, request: T) -> anyhow::Result<ApiResponse<T>>;
2557
2558 fn partial_update(&self, id: &uuid::Uuid, fields: HashMap<String, serde_json::Value>) -> anyhow::Result<ApiResponse<T>>;
2560
2561 fn soft_delete(&self, id: &uuid::Uuid) -> anyhow::Result<ApiResponse<()>>;
2563
2564 fn bulk_create(&self, request: BulkCreateRequest<T>) -> anyhow::Result<ApiResponse<BulkResponse<T>>>;
2566
2567 fn upsert(&self, request: UpsertRequest<T>) -> anyhow::Result<ApiResponse<T>>;
2569
2570 fn list_deleted(&self, request: ListRequest) -> anyhow::Result<ApiResponse<Vec<T>>>;
2572
2573 fn restore(&self, id: &uuid::Uuid) -> anyhow::Result<ApiResponse<T>>;
2575
2576 fn empty_trash(&self) -> anyhow::Result<ApiResponse<()>>;
2578
2579 fn get_deleted_by_id(&self, id: &uuid::Uuid) -> anyhow::Result<ApiResponse<T>>;
2581}
2582
2583#[derive(Debug, Serialize, Deserialize)]
2589#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
2590pub struct PaginationRequest {
2591 pub page: u32,
2592 pub limit: u32,
2593 pub sort_by: Option<String>,
2594 pub sort_order: Option<String>,
2595}
2596
2597fn camel_to_snake_case(key: &str) -> String {
2613 let chars: Vec<char> = key.chars().collect();
2614 let mut result = String::with_capacity(key.len() + 2);
2615 for (i, &c) in chars.iter().enumerate() {
2616 if c.is_ascii_uppercase() {
2617 let prev = if i > 0 { chars[i - 1] } else { '\0' };
2618 let next = chars.get(i + 1).copied().unwrap_or('\0');
2619 let crosses_lower = prev.is_ascii_lowercase() || prev.is_ascii_digit();
2623 let crosses_acronym = prev.is_ascii_uppercase() && next.is_ascii_lowercase();
2624 if (crosses_lower || crosses_acronym)
2625 && !result.is_empty()
2626 && !result.ends_with('_')
2627 {
2628 result.push('_');
2629 }
2630 result.push(c.to_ascii_lowercase());
2631 } else {
2632 result.push(c);
2633 }
2634 }
2635 result
2636}
2637
2638#[cfg(test)]
2639mod tests {
2640 use super::*;
2641
2642 #[test]
2643 fn snake_case_input_passes_through_unchanged() {
2644 assert_eq!(camel_to_snake_case("is_vip"), "is_vip");
2645 assert_eq!(camel_to_snake_case("flag_reason"), "flag_reason");
2646 assert_eq!(camel_to_snake_case("loyalty_tier_v2"), "loyalty_tier_v2");
2647 }
2648
2649 #[test]
2650 fn single_word_unchanged() {
2651 assert_eq!(camel_to_snake_case("name"), "name");
2652 assert_eq!(camel_to_snake_case("id"), "id");
2653 assert_eq!(camel_to_snake_case(""), "");
2654 }
2655
2656 #[test]
2657 fn camel_case_converts() {
2658 assert_eq!(camel_to_snake_case("isVip"), "is_vip");
2659 assert_eq!(camel_to_snake_case("userId"), "user_id");
2660 assert_eq!(camel_to_snake_case("customerSegment"), "customer_segment");
2661 assert_eq!(camel_to_snake_case("blacklistReason"), "blacklist_reason");
2662 }
2663
2664 #[test]
2665 fn pascal_case_converts() {
2666 assert_eq!(camel_to_snake_case("IsVip"), "is_vip");
2667 assert_eq!(camel_to_snake_case("UserId"), "user_id");
2668 }
2669
2670 #[test]
2671 fn acronym_runs_stay_together() {
2672 assert_eq!(camel_to_snake_case("IOError"), "io_error");
2675 assert_eq!(camel_to_snake_case("httpURL"), "http_url");
2676 assert_eq!(camel_to_snake_case("ABC"), "abc");
2677 assert_eq!(camel_to_snake_case("XMLHttpRequest"), "xml_http_request");
2678 }
2679
2680 #[test]
2681 fn digits_count_as_lowercase_for_boundary() {
2682 assert_eq!(camel_to_snake_case("v2Field"), "v2_field");
2683 assert_eq!(camel_to_snake_case("field2Name"), "field2_name");
2684 }
2685
2686 #[test]
2687 fn underscores_not_doubled() {
2688 assert_eq!(camel_to_snake_case("_Foo"), "_foo");
2689 assert_eq!(camel_to_snake_case("foo_Bar"), "foo_bar");
2690 }
2691
2692 #[test]
2693 fn shallow_pages_are_allowed() {
2694 assert!(pagination_depth_error(1, 100).is_none());
2696 assert!(pagination_depth_error(101, 100).is_none());
2698 }
2699
2700 #[test]
2701 fn pages_past_the_cap_are_rejected() {
2702 assert!(pagination_depth_error(102, 100).is_some());
2704 assert!(pagination_depth_error(1002, 10).is_some());
2706 }
2707
2708 #[test]
2709 fn oversized_page_size_is_clamped_before_the_check() {
2710 assert!(pagination_depth_error(102, 200).is_some());
2712 assert!(pagination_depth_error(101, 200).is_none());
2714 }
2715
2716 #[test]
2717 fn huge_page_number_does_not_overflow() {
2718 assert!(pagination_depth_error(u32::MAX, u32::MAX).is_some());
2720 }
2721
2722 #[test]
2725 fn batch_size_within_limit_is_allowed() {
2726 assert!(batch_size_error(0).is_none());
2727 assert!(batch_size_error(MAX_BATCH_SIZE).is_none());
2728 }
2729
2730 #[test]
2731 fn batch_size_over_limit_is_rejected() {
2732 assert!(batch_size_error(MAX_BATCH_SIZE + 1).is_some());
2733 }
2734
2735 #[test]
2736 fn a_bulk_preview_sorts_rows_by_what_the_patch_would_do() {
2737 use serde_json::json;
2738 let mut patched: HashMap<String, Vec<String>> = ["a", "b", "c"]
2739 .iter()
2740 .map(|id| (id.to_string(), vec!["category".to_string()]))
2741 .collect();
2742 patched.insert("d".into(), vec!["category_id".into()]);
2743 let out = preview_answer(
2744 vec![
2745 (
2746 "a".into(),
2747 Ok((
2748 json!({"category": "meal", "updated_at": "1"}),
2749 json!({"category": "travel", "updated_at": "2"}),
2750 )),
2751 ),
2752 ("b".into(), Ok((json!({"category": "travel"}), json!({"category": "travel"})))),
2753 (
2754 "d".into(),
2755 Ok((json!({"categoryId": "x"}), json!({"categoryId": "y"}))),
2756 ),
2757 ("c".into(), Err(("locked".into(), "a posted claim is locked".into()))),
2758 ],
2759 &patched,
2760 );
2761 assert_eq!(out.total, 4);
2762 assert_eq!(out.will_change.len(), 2);
2763 assert_eq!(out.will_change[1].changes[0].field, "category_id", "a camelCase response is read");
2764 let ch = &out.will_change[0].changes;
2765 assert_eq!(ch.len(), 1, "only the patched field is reported, not the timestamp");
2766 assert_eq!((ch[0].from.clone(), ch[0].to.clone()), (json!("meal"), json!("travel")));
2767 assert_eq!(out.unchanged, vec!["b".to_string()]);
2768 assert_eq!(out.refused[0].code, "locked");
2769 }
2770
2771 #[test]
2772 fn bulk_patch_request_parses_shared_shape() {
2773 let json = r#"{ "ids": ["a", "b"], "patch": { "status": "void" } }"#;
2774 let req: BulkPatchRequest = serde_json::from_str(json).unwrap();
2775 let items = req.into_items();
2776 assert_eq!(items.len(), 2);
2777 assert_eq!(items[0].1.get("status").unwrap(), "void");
2779 assert_eq!(items[1].1.get("status").unwrap(), "void");
2780 assert_eq!(items[0].0, "a");
2781 assert_eq!(items[1].0, "b");
2782 }
2783
2784 #[test]
2785 fn bulk_patch_request_parses_per_item_shape() {
2786 let json = r#"{ "items": [
2787 { "id": "a", "patch": { "status": "void" } },
2788 { "id": "b", "patch": { "note": "late" } }
2789 ] }"#;
2790 let req: BulkPatchRequest = serde_json::from_str(json).unwrap();
2791 let items = req.into_items();
2792 assert_eq!(items.len(), 2);
2793 assert_eq!(items[0].0, "a");
2794 assert_eq!(items[0].1.get("status").unwrap(), "void");
2795 assert_eq!(items[1].0, "b");
2796 assert_eq!(items[1].1.get("note").unwrap(), "late");
2797 }
2798
2799 #[test]
2800 fn batch_ids_request_parses() {
2801 let req: BatchIdsRequest = serde_json::from_str(r#"{ "ids": ["x", "y", "z"] }"#).unwrap();
2802 assert_eq!(req.ids, vec!["x", "y", "z"]);
2803 }
2804
2805 fn fields(q: &[(&str, &str)]) -> Vec<String> {
2808 let map: HashMap<String, String> =
2809 q.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect();
2810 sparse_fields(&map)
2811 }
2812
2813 #[test]
2814 fn sparse_fields_parses_comma_list_and_trims() {
2815 assert_eq!(fields(&[("fields", "id, name ,basePrice")]), vec!["id", "name", "basePrice"]);
2816 }
2817
2818 #[test]
2819 fn sparse_fields_absent_or_empty_is_no_projection() {
2820 assert!(fields(&[]).is_empty());
2821 assert!(fields(&[("fields", "")]).is_empty());
2822 assert!(fields(&[("fields", " , ")]).is_empty());
2823 }
2824
2825 #[test]
2826 fn project_keeps_requested_keys_plus_id() {
2827 let v = serde_json::json!({ "id": "1", "name": "n", "basePrice": 10, "hppPerUnit": 5 });
2828 let out = project_sparse(v, &["name".into(), "basePrice".into()]);
2829 assert_eq!(out, serde_json::json!({ "id": "1", "name": "n", "basePrice": 10 }));
2830 }
2831
2832 #[test]
2833 fn project_always_includes_id_even_if_not_requested() {
2834 let v = serde_json::json!({ "id": "1", "name": "n" });
2835 let out = project_sparse(v, &["name".into()]);
2836 assert_eq!(out, serde_json::json!({ "id": "1", "name": "n" }));
2837 }
2838
2839 #[test]
2840 fn project_ignores_unknown_keys() {
2841 let v = serde_json::json!({ "id": "1", "name": "n" });
2842 let out = project_sparse(v, &["name".into(), "nope".into()]);
2843 assert_eq!(out, serde_json::json!({ "id": "1", "name": "n" }));
2844 }
2845
2846 #[test]
2847 fn project_empty_fields_returns_full_object() {
2848 let v = serde_json::json!({ "id": "1", "name": "n", "x": 2 });
2849 let out = project_sparse(v.clone(), &[]);
2850 assert_eq!(out, v);
2851 }
2852
2853 #[test]
2854 fn project_non_object_returned_unchanged() {
2855 let v = serde_json::json!("scalar");
2856 assert_eq!(project_sparse(v.clone(), &["name".into()]), v);
2857 }
2858
2859 fn owner_a() -> uuid::Uuid {
2864 uuid::Uuid::parse_str("11111111-1111-4111-8111-111111111111").unwrap()
2865 }
2866 fn owner_b() -> uuid::Uuid {
2867 uuid::Uuid::parse_str("22222222-2222-4222-8222-222222222222").unwrap()
2868 }
2869
2870 fn row() -> serde_json::Value {
2871 serde_json::json!({ "id": "1", "providerId": owner_a().to_string(), "name": "n", "hppPerUnit": 5 })
2872 }
2873 const PRIV: &[&str] = &["hppPerUnit"];
2874
2875 struct Secretive;
2877 impl backbone_orm::EntityRepoMeta for Secretive {
2878 fn column_types() -> HashMap<String, String> {
2879 HashMap::new()
2880 }
2881 fn search_fields() -> &'static [&'static str] {
2882 &[]
2883 }
2884 fn secret_fields() -> &'static [&'static str] {
2885 &["tokenHash"]
2886 }
2887 fn private_fields() -> &'static [&'static str] {
2888 PRIV
2889 }
2890 fn owner_field() -> Option<&'static str> {
2891 Some("providerId")
2892 }
2893 fn relation_secret_fields(relation: &str) -> &'static [&'static str] {
2894 match relation {
2895 "user" => &["passwordHash"],
2896 _ => &[],
2897 }
2898 }
2899 }
2900
2901 fn secret_row() -> serde_json::Value {
2902 let mut r = row();
2903 r["tokenHash"] = serde_json::json!("digest");
2904 r
2905 }
2906
2907 #[test]
2908 fn a_secret_is_served_to_no_caller_not_even_platform_or_the_owner() {
2909 for scope in [None, Some(AccessScope::Platform), Some(AccessScope::Company(owner_a()))] {
2910 let out = secure::<Secretive>(secret_row(), scope.as_ref());
2911 assert!(out.get("tokenHash").is_none(), "{scope:?} was served the secret");
2912 assert!(out.get("name").is_some());
2913 }
2914 let platform = secure::<Secretive>(secret_row(), Some(&AccessScope::Platform));
2915 assert!(platform.get("hppPerUnit").is_some(), "private fields keep their owner/platform rule");
2916 }
2917
2918 #[test]
2919 fn a_request_that_names_a_secret_as_a_column_is_caught_in_every_grammar() {
2920 let ask = |pairs: &[(&str, &str)], sort_by: Option<&str>| {
2921 let params = ListQueryParams {
2922 filters: pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(),
2923 sort_by: sort_by.map(str::to_string),
2924 ..Default::default()
2925 };
2926 named_secret(¶ms, <Secretive as backbone_orm::EntityRepoMeta>::secret_fields())
2927 };
2928 for pairs in [
2929 vec![("token_hash", "abc")],
2930 vec![("tokenHash[startwith]", "a")],
2931 vec![("orderby", "name,-token_hash")],
2932 vec![("orderby[token_hash]", "asc")],
2933 vec![("searchFields", "name,tokenHash")],
2934 vec![("min", "token_hash")],
2935 vec![("group_by", "tokenHash")],
2936 ] {
2937 assert_eq!(ask(&pairs, None).as_deref().map(camel_to_snake_case), Some("token_hash".into()), "{pairs:?}");
2938 }
2939 assert!(ask(&[], Some("tokenHash")).is_some(), "sort_by");
2940 assert!(ask(&[("name[contain]", "token_hash"), ("orderby", "name")], None).is_none(), "a value is not a column");
2941 }
2942
2943 #[test]
2944 fn a_write_response_keeps_secrets_and_private_fields_from_every_caller() {
2945 let out = secure::<Secretive>(secret_row(), None);
2946 assert!(out.get("tokenHash").is_none() && out.get("hppPerUnit").is_none());
2947 }
2948
2949 #[test]
2950 fn a_routed_response_dto_loses_the_entitys_secrets() {
2951 let out = without_secrets::<Secretive, _>(secret_row());
2952 assert!(out.get("tokenHash").is_none() && out.get("hppPerUnit").is_none());
2953 assert!(out.get("name").is_some());
2954 }
2955
2956 #[test]
2957 fn an_included_row_loses_the_related_models_secrets() {
2958 let raw = serde_json::json!({ "id": "u1", "email": "a@b.c", "password_hash": "$argon2id$..." });
2959 let out = related_row::<Secretive>("user", "users", raw.clone());
2960 assert!(out.get("passwordHash").is_none(), "{out}");
2961 assert_eq!(out["email"], "a@b.c");
2962 assert!(related_row::<Secretive>("other", "others", raw.clone()).get("passwordHash").is_some(), "only the named relation");
2963 backbone_orm::secret_registry::register("employee_probe.staff", &["passwordHash"]);
2965 assert!(related_row::<Secretive>("staff", "employee_probe.staff", raw).get("passwordHash").is_none(), "registered table");
2966 }
2967
2968 #[test]
2969 fn security_no_private_fields_is_noop() {
2970 let v = row();
2971 assert_eq!(apply_field_security(v.clone(), None, &[], Some("providerId")), v);
2972 }
2973
2974 #[test]
2975 fn security_platform_sees_private() {
2976 let out = apply_field_security(row(), Some(&AccessScope::Platform), PRIV, Some("providerId"));
2977 assert!(out.get("hppPerUnit").is_some());
2978 }
2979
2980 #[test]
2981 fn security_owner_tenant_sees_private() {
2982 let out = apply_field_security(
2983 row(),
2984 Some(&AccessScope::Company(owner_a())),
2985 PRIV,
2986 Some("providerId"),
2987 );
2988 assert!(out.get("hppPerUnit").is_some());
2989 }
2990
2991 #[test]
2992 fn security_other_tenant_stripped() {
2993 let out = apply_field_security(
2994 row(),
2995 Some(&AccessScope::Company(owner_b())),
2996 PRIV,
2997 Some("providerId"),
2998 );
2999 assert!(out.get("hppPerUnit").is_none());
3000 assert!(out.get("name").is_some());
3001 }
3002
3003 #[test]
3004 fn security_absent_scope_fails_closed() {
3005 let out = apply_field_security(row(), None, PRIV, Some("providerId"));
3006 assert!(out.get("hppPerUnit").is_none());
3007 }
3008
3009 #[test]
3012 fn include_relations_parses_include_and_with() {
3013 let mut q = HashMap::new();
3014 q.insert("include".to_string(), "provider, outlet ".to_string());
3015 assert_eq!(include_relations(&q), vec!["provider", "outlet"]);
3016 let mut q2 = HashMap::new();
3017 q2.insert("with".to_string(), "category".to_string());
3018 assert_eq!(include_relations(&q2), vec!["category"]);
3019 assert!(include_relations(&HashMap::new()).is_empty());
3020 }
3021
3022 #[test]
3023 fn snake_to_camel_converts() {
3024 assert_eq!(snake_to_camel("provider_id"), "providerId");
3025 assert_eq!(snake_to_camel("business_name"), "businessName");
3026 assert_eq!(snake_to_camel("id"), "id");
3027 }
3028
3029 #[test]
3030 fn camelize_keys_top_level_only() {
3031 let v = serde_json::json!({ "provider_id": "1", "meta_data": { "created_at": "x" } });
3032 let out = camelize_keys(v);
3033 assert!(out.get("providerId").is_some());
3034 assert!(out.get("metaData").unwrap().get("created_at").is_some());
3036 }
3037
3038 #[test]
3039 fn security_null_owner_only_platform_sees() {
3040 let v = serde_json::json!({ "id": "1", "providerId": null, "hppPerUnit": 5 });
3041 let tenant = apply_field_security(v.clone(), Some(&AccessScope::Company(owner_a())), PRIV, Some("providerId"));
3042 assert!(tenant.get("hppPerUnit").is_none());
3043 let plat = apply_field_security(v, Some(&AccessScope::Platform), PRIV, Some("providerId"));
3044 assert!(plat.get("hppPerUnit").is_some());
3045 }
3046
3047 #[test]
3048 fn a_write_refused_for_named_reasons_answers_422_with_its_violations() {
3049 let v = vec![crate::violation::Violation::new("status", "field_not_writable", "`status` changes only through its verbs")];
3050 let (status, axum::Json(body)) =
3051 write_error::<()>(Some(v), &"validation failed: `status` changes only through its verbs", axum::http::StatusCode::BAD_REQUEST);
3052 assert_eq!(status, axum::http::StatusCode::UNPROCESSABLE_ENTITY);
3053 let json = serde_json::to_value(&body).unwrap();
3054 assert_eq!(json["error"], "validation failed: `status` changes only through its verbs");
3055 assert_eq!(json["violations"][0]["path"], "status");
3056 assert_eq!(json["violations"][0]["code"], "field_not_writable");
3057
3058 let (status, axum::Json(body)) = write_error::<()>(None, &"boom", axum::http::StatusCode::BAD_REQUEST);
3060 assert_eq!(status, axum::http::StatusCode::BAD_REQUEST);
3061 assert!(serde_json::to_value(&body).unwrap().get("violations").is_none());
3062 }
3063}
3064
3065#[cfg(test)]
3066mod internal_key_tests {
3067 use super::*;
3068
3069 fn params_with(filters: &[(&str, &str)]) -> ListQueryParams {
3070 ListQueryParams {
3071 page: 1,
3072 limit: 20,
3073 sort_by: None,
3074 sort_order: None,
3075 search: None,
3076 status: None,
3077 filters: filters
3078 .iter()
3079 .map(|(k, v)| (k.to_string(), v.to_string()))
3080 .collect(),
3081 }
3082 }
3083
3084 #[test]
3085 fn internal_keys_never_reach_the_repository() {
3086 let params = params_with(&[
3087 ("__base_condition", "EXISTS(SELECT 1)"),
3088 ("__base_condition[eq]", "1=1"),
3089 ("__anything", "x"),
3090 ("name", "acme"),
3091 ]);
3092 let filters = repository_filters(¶ms);
3093 assert_eq!(filters.len(), 1, "{filters:?}");
3094 assert_eq!(filters.get("name").map(String::as_str), Some("acme"));
3095 let filters = aggregate_filters(¶ms);
3096 assert!(filters.keys().all(|k| !k.starts_with("__")), "{filters:?}");
3097 }
3098}
3099
3100#[cfg(test)]
3101mod service_error_response_tests {
3102 use super::*;
3103 use crate::service::ServiceError;
3104
3105 #[test]
3106 fn a_refused_field_is_422_with_its_violation_never_500() {
3107 let err = ServiceError::Violations(vec![crate::violation::Violation::new(
3108 "state",
3109 "field_not_writable",
3110 "state cannot be changed by a generic write",
3111 )]);
3112 let (status, body) = service_error_response::<()>(&err);
3113 assert_eq!(status, axum::http::StatusCode::UNPROCESSABLE_ENTITY);
3114 assert_eq!(body.0.violations.as_ref().map(Vec::len), Some(1));
3115 }
3116
3117 #[test]
3118 fn other_errors_keep_their_own_status() {
3119 use axum::http::StatusCode;
3120 let status = |e: ServiceError| service_error_response::<()>(&e).0;
3121 assert_eq!(
3122 status(ServiceError::Validation("bad".into())),
3123 StatusCode::BAD_REQUEST
3124 );
3125 assert_eq!(status(ServiceError::NotFound), StatusCode::NOT_FOUND);
3126 assert_eq!(
3127 status(ServiceError::AlreadyExists("x".into())),
3128 StatusCode::CONFLICT
3129 );
3130 assert_eq!(
3131 status(ServiceError::Internal("boom".into())),
3132 StatusCode::INTERNAL_SERVER_ERROR
3133 );
3134 }
3135}