1use crate::extractors::JsonOrForm;
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
101fn write_error<T>(
104 violations: Option<Vec<crate::violation::Violation>>,
105 err: &impl std::fmt::Display,
106 fallback: axum::http::StatusCode,
107) -> (axum::http::StatusCode, axum::Json<ApiResponse<T>>) {
108 match violations {
109 Some(v) => (axum::http::StatusCode::UNPROCESSABLE_ENTITY, axum::Json(ApiResponse::rejected(err.to_string(), v))),
110 None => (fallback, axum::Json(ApiResponse::error(err.to_string()))),
111 }
112}
113
114#[derive(Debug, Deserialize, Default, Clone)]
116#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
117pub struct ListQueryParams {
118 #[serde(default = "default_page")]
119 pub page: u32,
120 #[serde(default = "default_limit")]
121 pub limit: u32,
122 #[serde(default)]
123 pub sort_by: Option<String>,
124 #[serde(default)]
125 pub sort_order: Option<String>,
126 #[serde(default)]
127 pub search: Option<String>,
128 #[serde(default)]
129 pub status: Option<String>,
130 #[serde(flatten)]
131 pub filters: HashMap<String, String>,
132}
133
134fn default_page() -> u32 { 1 }
135fn default_limit() -> u32 { 20 }
136
137const RESERVED_QUERY_KEYS: [&str; 3] = ["fields", "include", "with"];
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct HistoryEntry {
155 pub occurred_at: String,
156 pub action: String,
158 pub actor: String,
159 pub changed: serde_json::Value,
160 pub reason: Option<String>,
161 pub correlation_id: Option<String>,
162}
163
164#[async_trait::async_trait]
171pub trait HistoryProvider: Send + Sync {
172 async fn history(
180 &self,
181 table: &str,
182 id: &str,
183 limit: u32,
184 offset: u32,
185 ) -> Result<Option<Vec<HistoryEntry>>, String>;
186}
187
188const AGGREGATE_QUERY_KEYS: [&str; 7] =
194 ["group_by", "sum", "avg", "min", "max", "group_limit", "group_label"];
195
196fn aggregate_spec(params: &ListQueryParams) -> backbone_orm::repository::AggregateSpec {
202 use backbone_orm::repository::{AggregateFn, AggregateSpec};
203
204 let mut reductions = Vec::new();
205 for (key, func) in [
206 ("sum", AggregateFn::Sum),
207 ("avg", AggregateFn::Avg),
208 ("min", AggregateFn::Min),
209 ("max", AggregateFn::Max),
210 ] {
211 if let Some(raw) = params.filters.get(key) {
212 for field in raw.split(',').map(str::trim).filter(|f| !f.is_empty()) {
213 reductions.push((func, field.to_string()));
214 }
215 }
216 }
217
218 AggregateSpec {
219 group_by: params
220 .filters
221 .get("group_by")
222 .map(|g| g.trim().to_string())
223 .filter(|g| !g.is_empty()),
224 reductions,
225 group_limit: params
226 .filters
227 .get("group_limit")
228 .and_then(|l| l.trim().parse::<usize>().ok())
229 .unwrap_or(0),
230 label_field: params
231 .filters
232 .get("group_label")
233 .map(|l| l.trim().to_string())
234 .filter(|l| !l.is_empty()),
235 label_relation: None,
236 }
237}
238
239fn repository_filters(params: &ListQueryParams) -> HashMap<String, String> {
250 let mut filters = params.filters.clone();
251 for key in RESERVED_QUERY_KEYS {
252 filters.remove(key);
253 }
254 if let Some(search) = params.search.clone() {
255 filters.insert("search".to_string(), search);
256 }
257 if let Some(status) = params.status.clone() {
258 filters.insert("status".to_string(), status);
259 }
260 filters
261}
262
263fn aggregate_filters(params: &ListQueryParams) -> HashMap<String, String> {
266 let mut filters = repository_filters(params);
267 for key in AGGREGATE_QUERY_KEYS {
268 filters.remove(key);
269 }
270 filters
271}
272
273fn sparse_fields(query: &HashMap<String, String>) -> Vec<String> {
274 query
275 .get("fields")
276 .map(|s| {
277 s.split(',')
278 .map(str::trim)
279 .filter(|f| !f.is_empty())
280 .map(str::to_string)
281 .collect()
282 })
283 .unwrap_or_default()
284}
285
286fn to_response_value<R: Serialize>(r: R) -> serde_json::Value {
289 serde_json::to_value(r).unwrap_or(serde_json::Value::Null)
290}
291
292fn project_sparse(mut value: serde_json::Value, fields: &[String]) -> serde_json::Value {
296 if fields.is_empty() {
297 return value;
298 }
299 if let serde_json::Value::Object(map) = &mut value {
300 map.retain(|k, _| k == "id" || fields.iter().any(|f| f == k));
301 }
302 value
303}
304
305#[derive(Debug, Clone, Copy, PartialEq, Eq)]
328pub enum AccessScope {
329 Platform,
331 Company(uuid::Uuid),
333}
334
335impl AccessScope {
336 pub fn company(&self) -> Option<uuid::Uuid> {
342 match self {
343 AccessScope::Platform => None,
344 AccessScope::Company(id) => Some(*id),
345 }
346 }
347}
348
349fn apply_field_security(
355 mut value: serde_json::Value,
356 scope: Option<&AccessScope>,
357 private_fields: &[&str],
358 owner_field: Option<&str>,
359) -> serde_json::Value {
360 if private_fields.is_empty() {
361 return value;
362 }
363 let can_see_private = match scope {
364 Some(AccessScope::Platform) => true,
365 Some(AccessScope::Company(id)) => owner_field
366 .and_then(|f| value.get(f))
367 .and_then(|v| v.as_str())
368 .and_then(|owner| uuid::Uuid::parse_str(owner).ok())
372 .is_some_and(|owner| owner == *id),
373 None => false,
374 };
375 if !can_see_private {
376 if let serde_json::Value::Object(map) = &mut value {
377 for f in private_fields {
378 map.remove(*f);
379 }
380 }
381 }
382 value
383}
384
385fn include_relations(query: &HashMap<String, String>) -> Vec<String> {
387 query
388 .get("include")
389 .or_else(|| query.get("with"))
390 .map(|s| {
391 s.split(',')
392 .map(str::trim)
393 .filter(|s| !s.is_empty())
394 .map(str::to_string)
395 .collect()
396 })
397 .unwrap_or_default()
398}
399
400fn snake_to_camel(s: &str) -> String {
401 let mut out = String::with_capacity(s.len());
402 let mut upper = false;
403 for c in s.chars() {
404 if c == '_' {
405 upper = true;
406 } else if upper {
407 out.extend(c.to_uppercase());
408 upper = false;
409 } else {
410 out.push(c);
411 }
412 }
413 out
414}
415
416fn camelize_keys(v: serde_json::Value) -> serde_json::Value {
419 match v {
420 serde_json::Value::Object(m) => serde_json::Value::Object(
421 m.into_iter().map(|(k, val)| (snake_to_camel(&k), val)).collect(),
422 ),
423 other => other,
424 }
425}
426
427async fn expand_includes<S, E, C, U>(
437 service: &S,
438 rows: &mut [serde_json::Value],
439 includes: &[String],
440) where
441 S: CrudService<E, C, U>,
442 E: backbone_orm::EntityRepoMeta + Send + Sync + 'static,
443 C: Send + Sync + 'static,
444 U: Send + Sync + 'static,
445{
446 if includes.is_empty() || rows.is_empty() {
447 return;
448 }
449 for (rel_name, table, fk_field) in E::relations() {
450 if !includes.iter().any(|i| i == rel_name) {
451 continue;
452 }
453 let mut ids: Vec<String> = rows
454 .iter()
455 .filter_map(|r| r.get(fk_field).and_then(|v| v.as_str()).map(str::to_string))
456 .collect();
457 ids.sort();
458 ids.dedup();
459 if ids.is_empty() {
460 continue;
461 }
462 let related = service.fetch_related_json(table, &ids).await;
463 let mut by_id: HashMap<String, serde_json::Value> = HashMap::new();
464 for obj in related {
465 if let Some(id) = obj.get("id").and_then(|v| v.as_str()).map(str::to_string) {
466 by_id.insert(id, camelize_keys(obj));
467 }
468 }
469 for r in rows.iter_mut() {
470 let related_obj = r
471 .get(fk_field)
472 .and_then(|v| v.as_str())
473 .and_then(|id| by_id.get(id).cloned())
474 .unwrap_or(serde_json::Value::Null);
475 if let serde_json::Value::Object(m) = r {
476 m.insert((*rel_name).to_string(), related_obj);
477 }
478 }
479 }
480}
481
482pub const MAX_PER_PAGE: u32 = 100;
486
487pub const MAX_PAGINATION_OFFSET: u32 = 10_000;
492
493fn pagination_depth_error(page: u32, limit: u32) -> Option<String> {
499 let effective_limit = limit.clamp(1, MAX_PER_PAGE);
500 let offset = page.max(1).saturating_sub(1).saturating_mul(effective_limit);
501 if offset > MAX_PAGINATION_OFFSET {
502 Some(format!(
503 "Result set too deep: offset {offset} exceeds the maximum of \
504 {MAX_PAGINATION_OFFSET}. Please add filters to narrow your search."
505 ))
506 } else {
507 None
508 }
509}
510
511pub use crate::service::MAX_BATCH_SIZE;
517
518fn batch_size_error(count: usize) -> Option<String> {
521 if count > MAX_BATCH_SIZE {
522 Some(format!(
523 "Batch too large: {count} items exceeds the maximum of {MAX_BATCH_SIZE}."
524 ))
525 } else {
526 None
527 }
528}
529
530fn is_bad_query_error(msg: &str) -> bool {
539 let m = msg.to_lowercase();
540 m.contains("does not exist")
541 || m.contains("invalid input syntax")
542 || m.contains("42703")
543 || m.contains("not a column of this entity")
547 || m.contains("needs a numeric column")
548}
549
550#[derive(Debug, Serialize, Deserialize, Clone)]
552#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
553pub struct PaginationResponse {
554 pub total: u64,
555 pub page: u32,
556 pub limit: u32,
557 pub total_pages: u32,
558 #[serde(skip_serializing_if = "Option::is_none")]
562 pub next_cursor: Option<String>,
563 #[serde(skip_serializing_if = "Option::is_none")]
565 pub prev_cursor: Option<String>,
566 #[serde(skip_serializing_if = "Option::is_none")]
568 pub has_more: Option<bool>,
569}
570
571impl PaginationResponse {
572 pub fn new(total: u64, page: u32, limit: u32) -> Self {
573 let total_pages = if limit == 0 { 0 } else { ((total as f64) / (limit as f64)).ceil() as u32 };
574 Self { total, page, limit, total_pages, next_cursor: None, prev_cursor: None, has_more: None }
575 }
576
577 pub fn from_info(info: &backbone_orm::repository::PaginationInfo) -> Self {
581 Self {
582 total: info.total,
583 page: info.page,
584 limit: info.per_page,
585 total_pages: info.total_pages,
586 next_cursor: info.next_cursor.clone(),
587 prev_cursor: info.prev_cursor.clone(),
588 has_more: info.has_more,
589 }
590 }
591}
592
593#[derive(Debug, Serialize)]
595#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
596pub struct PaginatedResponse<T> {
597 pub data: Vec<T>,
598 pub meta: PaginationResponse,
599}
600
601#[derive(Debug, Serialize)]
604#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
605pub struct PaginatedApiResponse<T> {
606 pub success: bool,
607 pub data: Vec<T>,
608 pub meta: PaginationResponse,
609 #[serde(skip_serializing_if = "Option::is_none")]
610 pub error: Option<String>,
611}
612
613impl<T> PaginatedApiResponse<T> {
614 pub fn ok_with_info(data: Vec<T>, info: &backbone_orm::repository::PaginationInfo) -> Self {
617 Self {
618 success: true,
619 data,
620 meta: PaginationResponse::from_info(info),
621 error: None,
622 }
623 }
624
625 pub fn ok(data: Vec<T>, total: u64, page: u32, limit: u32) -> Self {
627 Self {
628 success: true,
629 data,
630 meta: PaginationResponse::new(total, page, limit),
631 error: None,
632 }
633 }
634
635 pub fn from_paginated(resp: PaginatedResponse<T>) -> Self {
637 Self {
638 success: true,
639 data: resp.data,
640 meta: resp.meta,
641 error: None,
642 }
643 }
644
645 pub fn error(error: impl Into<String>) -> Self {
647 Self {
648 success: false,
649 data: Vec::new(),
650 meta: PaginationResponse::new(0, 0, 0),
651 error: Some(error.into()),
652 }
653 }
654}
655
656#[derive(Debug, Serialize, Deserialize)]
658#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
659pub struct BulkCreateRequest<T> {
660 pub items: Vec<T>,
661}
662
663#[derive(Debug, Serialize, Deserialize)]
665#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
666pub struct BulkResponse<T> {
667 pub items: Vec<T>,
668 pub total: usize,
669 pub failed: usize,
670 pub errors: Vec<String>,
671}
672
673#[derive(Debug, Serialize, Deserialize)]
675#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
676pub struct UpsertRequest<T> {
677 pub entity: T,
678 pub create_if_not_exists: bool,
679}
680
681#[derive(Debug, Serialize, Deserialize)]
684#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
685pub struct BatchIdsRequest {
686 pub ids: Vec<String>,
687}
688
689#[derive(Debug, Deserialize)]
692#[serde(bound = "U: DeserializeOwned")]
693#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
694pub struct BulkUpdateItem<U> {
695 pub id: String,
696 #[serde(flatten)]
700 #[cfg_attr(feature = "openapi", schema(value_type = Object))]
701 pub data: U,
702}
703
704#[derive(Debug, Deserialize)]
706#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
707pub struct BulkPatchItem {
708 pub id: String,
709 pub patch: HashMap<String, serde_json::Value>,
710}
711
712#[derive(Debug, Deserialize)]
715#[serde(untagged)]
716#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
717pub enum BulkPatchRequest {
718 Shared {
720 ids: Vec<String>,
721 patch: HashMap<String, serde_json::Value>,
722 },
723 PerItem { items: Vec<BulkPatchItem> },
725}
726
727impl BulkPatchRequest {
728 fn into_items(self) -> Vec<(String, HashMap<String, serde_json::Value>)> {
730 match self {
731 BulkPatchRequest::Shared { ids, patch } => {
732 ids.into_iter().map(|id| (id, patch.clone())).collect()
733 }
734 BulkPatchRequest::PerItem { items } => {
735 items.into_iter().map(|it| (it.id, it.patch)).collect()
736 }
737 }
738 }
739}
740
741#[derive(Debug, Serialize, Deserialize)]
743#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
744pub struct FilterOptions {
745 pub filters: HashMap<String, String>,
746 pub sort_by: Option<String>,
747 pub sort_order: Option<SortOrder>,
748}
749
750#[derive(Debug, Serialize, Deserialize, Clone, Default)]
752#[serde(rename_all = "lowercase")]
753#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
754pub enum SortOrder {
755 #[default]
756 Asc,
757 Desc,
758}
759
760#[derive(Debug, Serialize, Deserialize)]
762#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
763pub struct ListRequest {
764 pub page: Option<u32>,
765 pub limit: Option<u32>,
766 pub sort_by: Option<String>,
767 pub sort_order: Option<SortOrder>,
768 pub filters: Option<HashMap<String, String>>,
769}
770
771impl Default for ListRequest {
772 fn default() -> Self {
773 Self {
774 page: Some(1),
775 limit: Some(20),
776 sort_by: None,
777 sort_order: None,
778 filters: None,
779 }
780 }
781}
782
783#[async_trait::async_trait]
807pub trait CrudService<Entity, CreateDto, UpdateDto>: Send + Sync
808where
809 Entity: Send + Sync + 'static,
812 CreateDto: Send + Sync + 'static,
813 UpdateDto: Send + Sync + 'static,
814{
815 type Error: std::error::Error + Send + Sync;
817
818 fn violations_of(err: &Self::Error) -> Option<Vec<crate::violation::Violation>> {
821 let _ = err;
822 None
823 }
824
825 fn entity_name() -> &'static str;
827
828 async fn fetch_related_json(
831 &self,
832 _table: &str,
833 _ids: &[String],
834 ) -> Vec<serde_json::Value> {
835 Vec::new()
836 }
837
838 async fn list(&self, page: u32, limit: u32, filters: HashMap<String, String>) -> Result<(Vec<Entity>, u64), Self::Error>;
840
841 async fn list_with_info(
845 &self,
846 page: u32,
847 limit: u32,
848 filters: HashMap<String, String>,
849 ) -> Result<(Vec<Entity>, backbone_orm::repository::PaginationInfo), Self::Error> {
850 let (rows, total) = self.list(page, limit, filters).await?;
851 Ok((
852 rows,
853 backbone_orm::repository::PaginationInfo::new(page, limit, total),
854 ))
855 }
856
857 async fn create(&self, dto: CreateDto) -> Result<Entity, Self::Error>;
859
860 async fn get_by_id(&self, id: &str) -> Result<Option<Entity>, Self::Error>;
862
863 async fn update(&self, id: &str, dto: UpdateDto) -> Result<Option<Entity>, Self::Error>;
865
866 async fn partial_update(&self, id: &str, fields: HashMap<String, serde_json::Value>) -> Result<Option<Entity>, Self::Error>;
868
869 async fn soft_delete(&self, id: &str) -> Result<bool, Self::Error>;
871
872 async fn bulk_create(&self, items: Vec<CreateDto>) -> Result<Vec<Entity>, Self::Error>;
874
875 async fn upsert(&self, dto: CreateDto) -> Result<Entity, Self::Error>;
877
878 async fn list_deleted(&self, page: u32, limit: u32) -> Result<(Vec<Entity>, u64), Self::Error>;
880
881 async fn restore(&self, id: &str) -> Result<Option<Entity>, Self::Error>;
883
884 async fn empty_trash(&self) -> Result<u64, Self::Error>;
886
887 async fn get_deleted_by_id(&self, id: &str) -> Result<Option<Entity>, Self::Error>;
889
890 async fn permanent_delete(&self, id: &str) -> Result<bool, Self::Error>;
892
893 async fn list_deleted_filtered(&self, page: u32, limit: u32, filters: HashMap<String, String>) -> Result<(Vec<Entity>, u64), Self::Error>;
895
896 async fn count_active(&self) -> Result<u64, Self::Error>;
898
899 fn table_name(&self) -> Option<&str> {
906 None
907 }
908
909 async fn aggregate(
914 &self,
915 spec: &backbone_orm::repository::AggregateSpec,
916 filters: HashMap<String, String>,
917 ) -> Result<backbone_orm::repository::AggregateResult, Self::Error>;
918
919 async fn count_active_filtered(
932 &self,
933 filters: HashMap<String, String>,
934 ) -> Result<u64, Self::Error> {
935 self.list(1, 1, filters).await.map(|(_, total)| total)
936 }
937
938 async fn count_deleted(&self) -> Result<u64, Self::Error>;
940
941 async fn bulk_soft_delete(&self, ids: Vec<String>) -> Result<u64, Self::Error> {
950 let mut n = 0;
951 for id in ids {
952 if self.soft_delete(&id).await? {
953 n += 1;
954 }
955 }
956 Ok(n)
957 }
958
959 async fn bulk_restore(&self, ids: Vec<String>) -> Result<Vec<Entity>, Self::Error> {
961 let mut out = Vec::with_capacity(ids.len());
962 for id in ids {
963 if let Some(e) = self.restore(&id).await? {
964 out.push(e);
965 }
966 }
967 Ok(out)
968 }
969
970 async fn bulk_permanent_delete(&self, ids: Vec<String>) -> Result<u64, Self::Error> {
972 let mut n = 0;
973 for id in ids {
974 if self.permanent_delete(&id).await? {
975 n += 1;
976 }
977 }
978 Ok(n)
979 }
980
981 async fn restore_all(&self) -> Result<u64, Self::Error> {
986 Ok(0)
987 }
988
989 async fn bulk_update(&self, items: Vec<(String, UpdateDto)>) -> Result<Vec<Entity>, Self::Error> {
991 let mut out = Vec::with_capacity(items.len());
992 for (id, dto) in items {
993 if let Some(e) = self.update(&id, dto).await? {
994 out.push(e);
995 }
996 }
997 Ok(out)
998 }
999
1000 async fn bulk_partial_update(
1002 &self,
1003 items: Vec<(String, HashMap<String, serde_json::Value>)>,
1004 ) -> Result<Vec<Entity>, Self::Error> {
1005 let mut out = Vec::with_capacity(items.len());
1006 for (id, fields) in items {
1007 if let Some(e) = self.partial_update(&id, fields).await? {
1008 out.push(e);
1009 }
1010 }
1011 Ok(out)
1012 }
1013}
1014
1015pub struct BackboneCrudHandler<S, E, C, U, R>
1038where
1039 S: CrudService<E, C, U> + 'static,
1040 E: Serialize + Send + Sync + 'static,
1041 C: DeserializeOwned + Send + Sync + 'static,
1042 U: DeserializeOwned + Send + Sync + 'static,
1043 R: From<E> + Serialize + Send + Sync + 'static,
1044{
1045 service: Arc<S>,
1046 _phantom: std::marker::PhantomData<(E, C, U, R)>,
1047}
1048
1049impl<S, E, C, U, R> BackboneCrudHandler<S, E, C, U, R>
1050where
1051 S: CrudService<E, C, U> + 'static,
1052 E: Serialize + Send + Sync + Clone + backbone_orm::EntityRepoMeta + 'static,
1053 C: DeserializeOwned + Send + Sync + 'static,
1054 U: DeserializeOwned + Send + Sync + 'static,
1055 R: From<E> + Serialize + Send + Sync + 'static,
1056{
1057 pub fn new(service: Arc<S>) -> Self {
1058 Self {
1059 service,
1060 _phantom: std::marker::PhantomData,
1061 }
1062 }
1063
1064 pub fn routes(service: Arc<S>, base_path: &str) -> axum::Router<()>
1088 where
1089 S: Clone,
1090 {
1091 Self::read_routes(service.clone(), base_path)
1092 .merge(Self::write_routes(service, base_path))
1093 }
1094
1095 pub fn read_routes(service: Arc<S>, base_path: &str) -> axum::Router<()>
1109 where
1110 S: Clone,
1111 {
1112 use axum::{
1113 extract::{Path, Query},
1114 routing::get,
1115 Extension, Router,
1116 };
1117
1118 let handler = Arc::new(Self::new(service));
1119
1120 Router::new()
1121 .route(base_path, get({
1123 let h = handler.clone();
1124 move |query: Query<ListQueryParams>, access: Option<Extension<AccessScope>>| async move {
1125 Self::list_handler(h, query, access).await
1126 }
1127 }))
1128 .route(&format!("{}/trash", base_path), get({
1130 let h = handler.clone();
1131 move |query: Query<ListQueryParams>, access: Option<Extension<AccessScope>>| async move {
1132 Self::list_deleted_handler(h, query, access).await
1133 }
1134 }))
1135 .route(&format!("{}/:id", base_path), get({
1137 let h = handler.clone();
1138 move |path: Path<String>, query: Query<ListQueryParams>, access: Option<Extension<AccessScope>>| async move {
1139 Self::get_handler(h, path, query, access).await
1140 }
1141 }))
1142 .route(&format!("{}/:id/deleted", base_path), get({
1144 let h = handler.clone();
1145 move |path: Path<String>, query: Query<ListQueryParams>, access: Option<Extension<AccessScope>>| async move {
1146 Self::get_deleted_handler(h, path, query, access).await
1147 }
1148 }))
1149 .route(&format!("{}/:id/history", base_path), get({
1152 let h = handler.clone();
1153 move |path: axum::extract::Path<String>,
1154 query: axum::extract::Query<ListQueryParams>,
1155 provider: Option<axum::Extension<std::sync::Arc<dyn HistoryProvider>>>| async move {
1156 Self::history_handler(h, path, query, provider).await
1157 }
1158 }))
1159 .route(&format!("{}/aggregate", base_path), get({
1161 let h = handler.clone();
1162 move |query: axum::extract::Query<ListQueryParams>| async move {
1163 Self::aggregate_handler(h, query).await
1164 }
1165 }))
1166 .route(&format!("{}/count", base_path), get({
1167 let h = handler.clone();
1168 move |query: axum::extract::Query<ListQueryParams>| async move {
1169 Self::count_active_handler(h, query).await
1170 }
1171 }))
1172 .route(&format!("{}/trash/count", base_path), get({
1174 let h = handler.clone();
1175 move || async move {
1176 Self::count_deleted_handler(h).await
1177 }
1178 }))
1179 }
1180
1181 pub fn write_routes(service: Arc<S>, base_path: &str) -> axum::Router<()>
1198 where
1199 S: Clone,
1200 {
1201 use axum::{
1202 extract::Path,
1203 routing::{delete, patch, post, put},
1204 Router,
1205 };
1206
1207 let handler = Arc::new(Self::new(service));
1208
1209 Router::new()
1210 .route(base_path, post({
1212 let h = handler.clone();
1213 move |body: JsonOrForm<C>| async move {
1214 Self::create_handler(h, body).await
1215 }
1216 }))
1217 .route(&format!("{}/bulk", base_path), post({
1219 let h = handler.clone();
1220 move |body: JsonOrForm<Vec<C>>| async move {
1221 Self::bulk_create_handler(h, body).await
1222 }
1223 }))
1224 .route(&format!("{}/upsert", base_path), post({
1226 let h = handler.clone();
1227 move |body: JsonOrForm<C>| async move {
1228 Self::upsert_handler(h, body).await
1229 }
1230 }))
1231 .route(&format!("{}/delete/bulk", base_path), post({
1233 let h = handler.clone();
1234 move |body: JsonOrForm<BatchIdsRequest>| async move {
1235 Self::bulk_delete_handler(h, body).await
1236 }
1237 }))
1238 .route(&format!("{}/restore/bulk", base_path), post({
1240 let h = handler.clone();
1241 move |body: JsonOrForm<BatchIdsRequest>| async move {
1242 Self::bulk_restore_handler(h, body).await
1243 }
1244 }))
1245 .route(&format!("{}/restore/all", base_path), post({
1247 let h = handler.clone();
1248 move || async move {
1249 Self::restore_all_handler(h).await
1250 }
1251 }))
1252 .route(&format!("{}/trash/bulk", base_path), delete({
1255 let h = handler.clone();
1256 move |body: JsonOrForm<BatchIdsRequest>| async move {
1257 Self::bulk_permanent_delete_handler(h, body).await
1258 }
1259 }))
1260 .route(&format!("{}/bulk", base_path), put({
1262 let h = handler.clone();
1263 move |body: JsonOrForm<Vec<BulkUpdateItem<U>>>| async move {
1264 Self::bulk_update_handler(h, body).await
1265 }
1266 }))
1267 .route(&format!("{}/bulk", base_path), patch({
1269 let h = handler.clone();
1270 move |body: JsonOrForm<BulkPatchRequest>| async move {
1271 Self::bulk_patch_handler(h, body).await
1272 }
1273 }))
1274 .route(&format!("{}/empty", base_path), delete({
1276 let h = handler.clone();
1277 move || async move {
1278 Self::empty_trash_handler(h).await
1279 }
1280 }))
1281 .route(&format!("{}/trash/:id", base_path), delete({
1283 let h = handler.clone();
1284 move |path: Path<String>| async move {
1285 Self::permanent_delete_handler(h, path).await
1286 }
1287 }))
1288 .route(&format!("{}/:id", base_path), put({
1290 let h = handler.clone();
1291 move |path: Path<String>, body: JsonOrForm<U>| async move {
1292 Self::update_handler(h, path, body).await
1293 }
1294 }))
1295 .route(&format!("{}/:id", base_path), patch({
1297 let h = handler.clone();
1298 move |path: Path<String>, body: JsonOrForm<HashMap<String, serde_json::Value>>| async move {
1299 Self::partial_update_handler(h, path, body).await
1300 }
1301 }))
1302 .route(&format!("{}/:id", base_path), delete({
1304 let h = handler.clone();
1305 move |path: Path<String>| async move {
1306 Self::delete_handler(h, path).await
1307 }
1308 }))
1309 .route(&format!("{}/:id/restore", base_path), post({
1311 let h = handler.clone();
1312 move |path: Path<String>| async move {
1313 Self::restore_handler(h, path).await
1314 }
1315 }))
1316 }
1317
1318 async fn list_handler(
1323 handler: Arc<Self>,
1324 axum::extract::Query(params): axum::extract::Query<ListQueryParams>,
1325 access: Option<axum::Extension<AccessScope>>,
1326 ) -> impl axum::response::IntoResponse {
1327 use axum::{http::StatusCode, Json};
1328
1329 if let Some(err) = pagination_depth_error(params.page, params.limit) {
1330 return (StatusCode::BAD_REQUEST, Json(PaginatedApiResponse::<serde_json::Value>::error(err)));
1331 }
1332
1333 let fields = sparse_fields(¶ms.filters);
1336 let includes = include_relations(¶ms.filters);
1337 let scope = access.map(|axum::Extension(s)| s);
1338
1339 let filters = repository_filters(¶ms);
1340
1341 match handler
1342 .service
1343 .list_with_info(params.page, params.limit, filters)
1344 .await
1345 {
1346 Ok((entities, info)) => {
1347 let mut rows: Vec<serde_json::Value> = entities
1350 .into_iter()
1351 .map(|e| {
1352 apply_field_security(
1353 to_response_value(R::from(e)),
1354 scope.as_ref(),
1355 E::private_fields(),
1356 E::owner_field(),
1357 )
1358 })
1359 .collect();
1360 expand_includes::<S, E, C, U>(&*handler.service, &mut rows, &includes).await;
1361 let items: Vec<serde_json::Value> =
1362 rows.into_iter().map(|r| project_sparse(r, &fields)).collect();
1363 let response = PaginatedApiResponse::ok_with_info(items, &info);
1364 (StatusCode::OK, Json(response))
1365 }
1366 Err(e) => {
1367 let msg = e.to_string();
1368 if is_bad_query_error(&msg) {
1369 (StatusCode::BAD_REQUEST, Json(PaginatedApiResponse::<serde_json::Value>::error(
1370 format!("Invalid query parameter or filter: {msg}"),
1371 )))
1372 } else {
1373 (StatusCode::INTERNAL_SERVER_ERROR, Json(PaginatedApiResponse::<serde_json::Value>::error(msg)))
1374 }
1375 }
1376 }
1377 }
1378
1379 async fn create_handler(
1380 handler: Arc<Self>,
1381 JsonOrForm(dto): JsonOrForm<C>,
1382 ) -> impl axum::response::IntoResponse {
1383 use axum::{http::StatusCode, Json};
1384
1385 match handler.service.create(dto).await {
1386 Ok(entity) => {
1387 let response: R = entity.into();
1388 (StatusCode::CREATED, Json(ApiResponse::ok(response)))
1389 }
1390 Err(e) if S::violations_of(&e).is_some() => write_error(S::violations_of(&e), &e, StatusCode::BAD_REQUEST),
1391 Err(e) => {
1392 let error_str = e.to_string();
1393 if error_str.contains("conflict") || error_str.contains("already exists") {
1394 (StatusCode::CONFLICT, Json(ApiResponse::<R>::error(error_str)))
1395 } else {
1396 (StatusCode::BAD_REQUEST, Json(ApiResponse::<R>::error(error_str)))
1397 }
1398 }
1399 }
1400 }
1401
1402 async fn get_handler(
1403 handler: Arc<Self>,
1404 axum::extract::Path(id): axum::extract::Path<String>,
1405 axum::extract::Query(params): axum::extract::Query<ListQueryParams>,
1406 access: Option<axum::Extension<AccessScope>>,
1407 ) -> impl axum::response::IntoResponse {
1408 use axum::{http::StatusCode, Json};
1409
1410 let fields = sparse_fields(¶ms.filters);
1411 let includes = include_relations(¶ms.filters);
1412 let scope = access.map(|axum::Extension(s)| s);
1413
1414 match handler.service.get_by_id(&id).await {
1415 Ok(Some(entity)) => {
1416 let secured = apply_field_security(
1417 to_response_value(R::from(entity)),
1418 scope.as_ref(),
1419 E::private_fields(),
1420 E::owner_field(),
1421 );
1422 let mut rows = [secured];
1423 expand_includes::<S, E, C, U>(&*handler.service, &mut rows, &includes).await;
1424 let [secured] = rows;
1425 let value = project_sparse(secured, &fields);
1426 (StatusCode::OK, Json(ApiResponse::ok(value)))
1427 }
1428 Ok(None) => {
1429 (StatusCode::NOT_FOUND, Json(ApiResponse::<serde_json::Value>::not_found(S::entity_name(), &id)))
1430 }
1431 Err(e) => {
1432 (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<serde_json::Value>::error(e.to_string())))
1433 }
1434 }
1435 }
1436
1437 async fn update_handler(
1438 handler: Arc<Self>,
1439 axum::extract::Path(id): axum::extract::Path<String>,
1440 JsonOrForm(dto): JsonOrForm<U>,
1441 ) -> impl axum::response::IntoResponse {
1442 use axum::{http::StatusCode, Json};
1443
1444 match handler.service.update(&id, dto).await {
1445 Ok(Some(entity)) => {
1446 let response: R = entity.into();
1447 (StatusCode::OK, Json(ApiResponse::ok(response)))
1448 }
1449 Ok(None) => {
1450 (StatusCode::NOT_FOUND, Json(ApiResponse::<R>::not_found(S::entity_name(), &id)))
1451 }
1452 Err(e) => {
1453 write_error(S::violations_of(&e), &e, StatusCode::BAD_REQUEST)
1454 }
1455 }
1456 }
1457
1458 async fn partial_update_handler(
1459 handler: Arc<Self>,
1460 axum::extract::Path(id): axum::extract::Path<String>,
1461 JsonOrForm(fields): JsonOrForm<HashMap<String, serde_json::Value>>,
1462 ) -> impl axum::response::IntoResponse {
1463 use axum::{http::StatusCode, Json};
1464
1465 let fields: HashMap<String, serde_json::Value> = fields
1476 .into_iter()
1477 .map(|(k, v)| (camel_to_snake_case(&k), v))
1478 .collect();
1479
1480 match handler.service.partial_update(&id, fields).await {
1481 Ok(Some(entity)) => {
1482 let response: R = entity.into();
1483 (StatusCode::OK, Json(ApiResponse::ok(response)))
1484 }
1485 Ok(None) => {
1486 (StatusCode::NOT_FOUND, Json(ApiResponse::<R>::not_found(S::entity_name(), &id)))
1487 }
1488 Err(e) => {
1489 write_error(S::violations_of(&e), &e, StatusCode::BAD_REQUEST)
1490 }
1491 }
1492 }
1493
1494 async fn delete_handler(
1495 handler: Arc<Self>,
1496 axum::extract::Path(id): axum::extract::Path<String>,
1497 ) -> impl axum::response::IntoResponse {
1498 use axum::{http::StatusCode, Json};
1499
1500 match handler.service.soft_delete(&id).await {
1501 Ok(true) => {
1502 (StatusCode::OK, Json(ApiResponse::<()>::success_with_message((), "Entity deleted successfully")))
1505 }
1506 Ok(false) => {
1507 (StatusCode::NOT_FOUND, Json(ApiResponse::<()>::not_found(S::entity_name(), &id)))
1508 }
1509 Err(e) => {
1510 write_error(S::violations_of(&e), &e, StatusCode::INTERNAL_SERVER_ERROR)
1511 }
1512 }
1513 }
1514
1515 async fn bulk_create_handler(
1516 handler: Arc<Self>,
1517 JsonOrForm(items): JsonOrForm<Vec<C>>,
1518 ) -> impl axum::response::IntoResponse {
1519 use axum::{http::StatusCode, Json};
1520
1521 match handler.service.bulk_create(items).await {
1522 Ok(entities) => {
1523 let result_items: Vec<R> = entities.into_iter().map(R::from).collect();
1524 let total = result_items.len();
1525 let response = BulkResponse {
1526 items: result_items,
1527 total,
1528 failed: 0,
1529 errors: vec![],
1530 };
1531 (StatusCode::CREATED, Json(ApiResponse::ok(response)))
1532 }
1533 Err(e) => {
1534 write_error(S::violations_of(&e), &e, StatusCode::BAD_REQUEST)
1535 }
1536 }
1537 }
1538
1539 async fn bulk_delete_handler(
1542 handler: Arc<Self>,
1543 JsonOrForm(req): JsonOrForm<BatchIdsRequest>,
1544 ) -> impl axum::response::IntoResponse {
1545 use axum::{http::StatusCode, Json};
1546
1547 if let Some(err) = batch_size_error(req.ids.len()) {
1548 return (StatusCode::BAD_REQUEST, Json(ApiResponse::<serde_json::Value>::error(err)));
1549 }
1550 match handler.service.bulk_soft_delete(req.ids).await {
1551 Ok(count) => (StatusCode::OK, Json(ApiResponse::success_with_message(
1552 serde_json::json!({ "soft_deleted": count }),
1553 format!("Soft-deleted {count} item(s)"),
1554 ))),
1555 Err(e) => write_error(S::violations_of(&e), &e, StatusCode::BAD_REQUEST),
1556 }
1557 }
1558
1559 async fn bulk_restore_handler(
1560 handler: Arc<Self>,
1561 JsonOrForm(req): JsonOrForm<BatchIdsRequest>,
1562 ) -> impl axum::response::IntoResponse {
1563 use axum::{http::StatusCode, Json};
1564
1565 if let Some(err) = batch_size_error(req.ids.len()) {
1566 return (StatusCode::BAD_REQUEST, Json(ApiResponse::<BulkResponse<R>>::error(err)));
1567 }
1568 match handler.service.bulk_restore(req.ids).await {
1569 Ok(entities) => {
1570 let items: Vec<R> = entities.into_iter().map(R::from).collect();
1571 let total = items.len();
1572 (StatusCode::OK, Json(ApiResponse::ok(BulkResponse { items, total, failed: 0, errors: vec![] })))
1573 }
1574 Err(e) => write_error(S::violations_of(&e), &e, StatusCode::BAD_REQUEST),
1575 }
1576 }
1577
1578 async fn restore_all_handler(
1579 handler: Arc<Self>,
1580 ) -> impl axum::response::IntoResponse {
1581 use axum::{http::StatusCode, Json};
1582
1583 match handler.service.restore_all().await {
1584 Ok(count) => (StatusCode::OK, Json(ApiResponse::success_with_message(
1585 serde_json::json!({ "restored": count }),
1586 format!("Restored {count} item(s) from trash"),
1587 ))),
1588 Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<serde_json::Value>::error(e.to_string()))),
1589 }
1590 }
1591
1592 async fn bulk_permanent_delete_handler(
1593 handler: Arc<Self>,
1594 JsonOrForm(req): JsonOrForm<BatchIdsRequest>,
1595 ) -> impl axum::response::IntoResponse {
1596 use axum::{http::StatusCode, Json};
1597
1598 if let Some(err) = batch_size_error(req.ids.len()) {
1599 return (StatusCode::BAD_REQUEST, Json(ApiResponse::<serde_json::Value>::error(err)));
1600 }
1601 match handler.service.bulk_permanent_delete(req.ids).await {
1602 Ok(count) => (StatusCode::OK, Json(ApiResponse::success_with_message(
1603 serde_json::json!({ "permanently_deleted": count }),
1604 format!("Permanently deleted {count} item(s)"),
1605 ))),
1606 Err(e) => write_error(S::violations_of(&e), &e, StatusCode::BAD_REQUEST),
1607 }
1608 }
1609
1610 async fn bulk_update_handler(
1611 handler: Arc<Self>,
1612 JsonOrForm(items): JsonOrForm<Vec<BulkUpdateItem<U>>>,
1613 ) -> impl axum::response::IntoResponse {
1614 use axum::{http::StatusCode, Json};
1615
1616 if let Some(err) = batch_size_error(items.len()) {
1617 return (StatusCode::BAD_REQUEST, Json(ApiResponse::<BulkResponse<R>>::error(err)));
1618 }
1619 let items: Vec<(String, U)> = items.into_iter().map(|it| (it.id, it.data)).collect();
1620 match handler.service.bulk_update(items).await {
1621 Ok(entities) => {
1622 let items: Vec<R> = entities.into_iter().map(R::from).collect();
1623 let total = items.len();
1624 (StatusCode::OK, Json(ApiResponse::ok(BulkResponse { items, total, failed: 0, errors: vec![] })))
1625 }
1626 Err(e) => write_error(S::violations_of(&e), &e, StatusCode::BAD_REQUEST),
1627 }
1628 }
1629
1630 async fn bulk_patch_handler(
1631 handler: Arc<Self>,
1632 JsonOrForm(req): JsonOrForm<BulkPatchRequest>,
1633 ) -> impl axum::response::IntoResponse {
1634 use axum::{http::StatusCode, Json};
1635
1636 let items = req.into_items();
1637 if let Some(err) = batch_size_error(items.len()) {
1638 return (StatusCode::BAD_REQUEST, Json(ApiResponse::<BulkResponse<R>>::error(err)));
1639 }
1640 let items: Vec<(String, HashMap<String, serde_json::Value>)> = items
1642 .into_iter()
1643 .map(|(id, fields)| {
1644 let fields = fields
1645 .into_iter()
1646 .map(|(k, v)| (camel_to_snake_case(&k), v))
1647 .collect();
1648 (id, fields)
1649 })
1650 .collect();
1651 match handler.service.bulk_partial_update(items).await {
1652 Ok(entities) => {
1653 let items: Vec<R> = entities.into_iter().map(R::from).collect();
1654 let total = items.len();
1655 (StatusCode::OK, Json(ApiResponse::ok(BulkResponse { items, total, failed: 0, errors: vec![] })))
1656 }
1657 Err(e) => write_error(S::violations_of(&e), &e, StatusCode::BAD_REQUEST),
1658 }
1659 }
1660
1661 async fn upsert_handler(
1662 handler: Arc<Self>,
1663 JsonOrForm(dto): JsonOrForm<C>,
1664 ) -> impl axum::response::IntoResponse {
1665 use axum::{http::StatusCode, Json};
1666
1667 match handler.service.upsert(dto).await {
1668 Ok(entity) => {
1669 let response: R = entity.into();
1670 (StatusCode::OK, Json(ApiResponse::ok(response)))
1671 }
1672 Err(e) => {
1673 write_error(S::violations_of(&e), &e, StatusCode::BAD_REQUEST)
1674 }
1675 }
1676 }
1677
1678 async fn list_deleted_handler(
1679 handler: Arc<Self>,
1680 axum::extract::Query(params): axum::extract::Query<ListQueryParams>,
1681 access: Option<axum::Extension<AccessScope>>,
1682 ) -> impl axum::response::IntoResponse {
1683 use axum::{http::StatusCode, Json};
1684
1685 if let Some(err) = pagination_depth_error(params.page, params.limit) {
1686 return (StatusCode::BAD_REQUEST, Json(PaginatedApiResponse::<serde_json::Value>::error(err)));
1687 }
1688
1689 let fields = sparse_fields(¶ms.filters);
1690 let includes = include_relations(¶ms.filters);
1691 let scope = access.map(|axum::Extension(s)| s);
1692
1693 match handler.service.list_deleted(params.page, params.limit).await {
1694 Ok((entities, total)) => {
1695 let mut rows: Vec<serde_json::Value> = entities
1699 .into_iter()
1700 .map(|e| {
1701 apply_field_security(
1702 to_response_value(R::from(e)),
1703 scope.as_ref(),
1704 E::private_fields(),
1705 E::owner_field(),
1706 )
1707 })
1708 .collect();
1709 expand_includes::<S, E, C, U>(&*handler.service, &mut rows, &includes).await;
1710 let items: Vec<serde_json::Value> =
1711 rows.into_iter().map(|r| project_sparse(r, &fields)).collect();
1712 let response = PaginatedApiResponse::ok(items, total, params.page, params.limit);
1713 (StatusCode::OK, Json(response))
1714 }
1715 Err(e) => {
1716 let msg = e.to_string();
1717 if is_bad_query_error(&msg) {
1718 (StatusCode::BAD_REQUEST, Json(PaginatedApiResponse::<serde_json::Value>::error(
1719 format!("Invalid query parameter or filter: {msg}"),
1720 )))
1721 } else {
1722 (StatusCode::INTERNAL_SERVER_ERROR, Json(PaginatedApiResponse::<serde_json::Value>::error(msg)))
1723 }
1724 }
1725 }
1726 }
1727
1728 async fn restore_handler(
1729 handler: Arc<Self>,
1730 axum::extract::Path(id): axum::extract::Path<String>,
1731 ) -> impl axum::response::IntoResponse {
1732 use axum::{http::StatusCode, Json};
1733
1734 match handler.service.restore(&id).await {
1735 Ok(Some(entity)) => {
1736 let response: R = entity.into();
1737 (StatusCode::OK, Json(ApiResponse::success_with_message(response, "Entity restored successfully")))
1738 }
1739 Ok(None) => {
1740 (StatusCode::NOT_FOUND, Json(ApiResponse::<R>::not_found(S::entity_name(), &id)))
1741 }
1742 Err(e) => {
1743 write_error(S::violations_of(&e), &e, StatusCode::BAD_REQUEST)
1744 }
1745 }
1746 }
1747
1748 async fn empty_trash_handler(
1749 handler: Arc<Self>,
1750 ) -> impl axum::response::IntoResponse {
1751 use axum::{http::StatusCode, Json};
1752
1753 match handler.service.empty_trash().await {
1754 Ok(count) => {
1755 (StatusCode::OK, Json(ApiResponse::success_with_message(
1756 serde_json::json!({ "deleted_count": count }),
1757 format!("Successfully deleted {} items from trash", count)
1758 )))
1759 }
1760 Err(e) => {
1761 (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<serde_json::Value>::error(e.to_string())))
1762 }
1763 }
1764 }
1765
1766 async fn get_deleted_handler(
1768 handler: Arc<Self>,
1769 axum::extract::Path(id): axum::extract::Path<String>,
1770 axum::extract::Query(params): axum::extract::Query<ListQueryParams>,
1771 access: Option<axum::Extension<AccessScope>>,
1772 ) -> impl axum::response::IntoResponse {
1773 use axum::{http::StatusCode, Json};
1774
1775 let fields = sparse_fields(¶ms.filters);
1776 let scope = access.map(|axum::Extension(s)| s);
1777
1778 match handler.service.get_deleted_by_id(&id).await {
1779 Ok(Some(entity)) => {
1780 let secured = apply_field_security(
1781 to_response_value(R::from(entity)),
1782 scope.as_ref(),
1783 E::private_fields(),
1784 E::owner_field(),
1785 );
1786 let value = project_sparse(secured, &fields);
1787 (StatusCode::OK, Json(ApiResponse::ok(value)))
1788 }
1789 Ok(None) => {
1790 (StatusCode::NOT_FOUND, Json(ApiResponse::<serde_json::Value>::not_found(&format!("Deleted {}", S::entity_name()), &id)))
1791 }
1792 Err(e) => {
1793 (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<serde_json::Value>::error(e.to_string())))
1794 }
1795 }
1796 }
1797
1798 async fn permanent_delete_handler(
1800 handler: Arc<Self>,
1801 axum::extract::Path(id): axum::extract::Path<String>,
1802 ) -> axum::response::Response {
1803 use axum::{http::StatusCode, Json, response::IntoResponse};
1804
1805 match handler.service.get_deleted_by_id(&id).await {
1807 Ok(Some(_)) => {
1808 match handler.service.permanent_delete(&id).await {
1810 Ok(true) => {
1811 StatusCode::NO_CONTENT.into_response()
1813 }
1814 Ok(false) => {
1815 (StatusCode::NOT_FOUND, Json(ApiResponse::<R>::error(
1816 format!("Failed to permanently delete {}", S::entity_name())
1817 ))).into_response()
1818 }
1819 Err(e) => {
1820 write_error::<R>(S::violations_of(&e), &e, StatusCode::INTERNAL_SERVER_ERROR).into_response()
1821 }
1822 }
1823 }
1824 Ok(None) => {
1825 (StatusCode::NOT_FOUND, Json(ApiResponse::<R>::not_found(
1826 &format!("{} in trash", S::entity_name()), &id
1827 ))).into_response()
1828 }
1829 Err(e) => {
1830 write_error::<R>(S::violations_of(&e), &e, StatusCode::INTERNAL_SERVER_ERROR).into_response()
1831 }
1832 }
1833 }
1834
1835 async fn count_active_handler(
1837 handler: Arc<Self>,
1838 axum::extract::Query(params): axum::extract::Query<ListQueryParams>,
1839 ) -> impl axum::response::IntoResponse {
1840 use axum::{http::StatusCode, Json};
1841
1842 match handler.service.count_active_filtered(repository_filters(¶ms)).await {
1846 Ok(count) => {
1847 (StatusCode::OK, Json(ApiResponse::ok(serde_json::json!({ "count": count }))))
1848 }
1849 Err(e) => {
1850 let msg = e.to_string();
1853 let code = if is_bad_query_error(&msg) {
1854 StatusCode::BAD_REQUEST
1855 } else {
1856 StatusCode::INTERNAL_SERVER_ERROR
1857 };
1858 (code, Json(ApiResponse::<serde_json::Value>::error(msg)))
1859 }
1860 }
1861 }
1862
1863 async fn aggregate_handler(
1868 handler: Arc<Self>,
1869 axum::extract::Query(params): axum::extract::Query<ListQueryParams>,
1870 ) -> impl axum::response::IntoResponse {
1871 use axum::{http::StatusCode, Json};
1872
1873 let spec = aggregate_spec(¶ms);
1874 match handler.service.aggregate(&spec, aggregate_filters(¶ms)).await {
1875 Ok(result) => {
1876 let render = |g: &backbone_orm::repository::AggregateGroup| {
1877 let mut out = serde_json::Map::new();
1881 out.insert("key".into(), match &g.key {
1882 Some(k) => serde_json::Value::String(k.clone()),
1883 None => serde_json::Value::Null,
1884 });
1885 if g.label.is_some() {
1886 out.insert(
1887 "label".into(),
1888 serde_json::Value::String(g.label.clone().unwrap()),
1889 );
1890 }
1891 out.insert("count".into(), serde_json::json!(g.count));
1892 for (compound, value) in &g.values {
1893 let Some((func, field)) = compound.split_once(':') else { continue };
1894 let slot = out
1895 .entry(func.to_string())
1896 .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
1897 if let Some(obj) = slot.as_object_mut() {
1898 obj.insert(field.to_string(), match value {
1899 Some(v) => serde_json::Value::String(v.clone()),
1903 None => serde_json::Value::Null,
1904 });
1905 }
1906 }
1907 serde_json::Value::Object(out)
1908 };
1909
1910 let body = serde_json::json!({
1911 "groups": result.groups.iter().map(render).collect::<Vec<_>>(),
1912 "total": render(&result.total),
1913 "truncated": result.truncated,
1916 });
1917 (StatusCode::OK, Json(ApiResponse::ok(body)))
1918 }
1919 Err(e) => {
1920 let msg = e.to_string();
1921 let code = if is_bad_query_error(&msg) {
1922 StatusCode::BAD_REQUEST
1923 } else {
1924 StatusCode::INTERNAL_SERVER_ERROR
1925 };
1926 (code, Json(ApiResponse::<serde_json::Value>::error(msg)))
1927 }
1928 }
1929 }
1930
1931 async fn history_handler(
1942 handler: Arc<Self>,
1943 axum::extract::Path(id): axum::extract::Path<String>,
1944 axum::extract::Query(params): axum::extract::Query<ListQueryParams>,
1945 provider: Option<axum::Extension<std::sync::Arc<dyn HistoryProvider>>>,
1946 ) -> impl axum::response::IntoResponse {
1947 use axum::{http::StatusCode, Json};
1948
1949 let Some(axum::Extension(provider)) = provider else {
1950 return (
1951 StatusCode::NOT_IMPLEMENTED,
1952 Json(ApiResponse::<serde_json::Value>::error(
1953 "history is not configured for this service".to_string(),
1954 )),
1955 );
1956 };
1957
1958 let Some(table) = handler.service.table_name().map(|t| t.to_string()) else {
1962 return (
1963 StatusCode::NOT_IMPLEMENTED,
1964 Json(ApiResponse::<serde_json::Value>::error(
1965 "this entity cannot name its table, so its history cannot be keyed".to_string(),
1966 )),
1967 );
1968 };
1969
1970 let limit = params.limit.clamp(1, 200);
1971 let offset = params.page.saturating_sub(1) * limit;
1972
1973 match provider.history(&table, &id, limit, offset).await {
1974 Ok(Some(entries)) => (
1975 StatusCode::OK,
1976 Json(ApiResponse::ok(serde_json::json!({
1977 "audited": true,
1978 "entries": entries,
1979 }))),
1980 ),
1981 Ok(None) => (
1982 StatusCode::OK,
1983 Json(ApiResponse::ok(serde_json::json!({
1984 "audited": false,
1987 "entries": serde_json::Value::Null,
1988 }))),
1989 ),
1990 Err(e) => (
1991 StatusCode::INTERNAL_SERVER_ERROR,
1992 Json(ApiResponse::<serde_json::Value>::error(e)),
1993 ),
1994 }
1995 }
1996
1997 async fn count_deleted_handler(
1999 handler: Arc<Self>,
2000 ) -> impl axum::response::IntoResponse {
2001 use axum::{http::StatusCode, Json};
2002
2003 match handler.service.count_deleted().await {
2004 Ok(count) => {
2005 (StatusCode::OK, Json(ApiResponse::ok(serde_json::json!({ "count": count }))))
2006 }
2007 Err(e) => {
2008 (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<serde_json::Value>::error(e.to_string())))
2009 }
2010 }
2011 }
2012}
2013
2014pub trait BackboneHttpHandler<T>: Send + Sync {
2039 fn list(&self, request: ListRequest) -> anyhow::Result<ApiResponse<Vec<T>>>;
2041
2042 fn create(&self, request: T) -> anyhow::Result<ApiResponse<T>>;
2044
2045 fn get_by_id(&self, id: &uuid::Uuid) -> anyhow::Result<ApiResponse<T>>;
2047
2048 fn update(&self, id: &uuid::Uuid, request: T) -> anyhow::Result<ApiResponse<T>>;
2050
2051 fn partial_update(&self, id: &uuid::Uuid, fields: HashMap<String, serde_json::Value>) -> anyhow::Result<ApiResponse<T>>;
2053
2054 fn soft_delete(&self, id: &uuid::Uuid) -> anyhow::Result<ApiResponse<()>>;
2056
2057 fn bulk_create(&self, request: BulkCreateRequest<T>) -> anyhow::Result<ApiResponse<BulkResponse<T>>>;
2059
2060 fn upsert(&self, request: UpsertRequest<T>) -> anyhow::Result<ApiResponse<T>>;
2062
2063 fn list_deleted(&self, request: ListRequest) -> anyhow::Result<ApiResponse<Vec<T>>>;
2065
2066 fn restore(&self, id: &uuid::Uuid) -> anyhow::Result<ApiResponse<T>>;
2068
2069 fn empty_trash(&self) -> anyhow::Result<ApiResponse<()>>;
2071
2072 fn get_deleted_by_id(&self, id: &uuid::Uuid) -> anyhow::Result<ApiResponse<T>>;
2074}
2075
2076#[derive(Debug, Serialize, Deserialize)]
2082#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
2083pub struct PaginationRequest {
2084 pub page: u32,
2085 pub limit: u32,
2086 pub sort_by: Option<String>,
2087 pub sort_order: Option<String>,
2088}
2089
2090fn camel_to_snake_case(key: &str) -> String {
2106 let chars: Vec<char> = key.chars().collect();
2107 let mut result = String::with_capacity(key.len() + 2);
2108 for (i, &c) in chars.iter().enumerate() {
2109 if c.is_ascii_uppercase() {
2110 let prev = if i > 0 { chars[i - 1] } else { '\0' };
2111 let next = chars.get(i + 1).copied().unwrap_or('\0');
2112 let crosses_lower = prev.is_ascii_lowercase() || prev.is_ascii_digit();
2116 let crosses_acronym = prev.is_ascii_uppercase() && next.is_ascii_lowercase();
2117 if (crosses_lower || crosses_acronym)
2118 && !result.is_empty()
2119 && !result.ends_with('_')
2120 {
2121 result.push('_');
2122 }
2123 result.push(c.to_ascii_lowercase());
2124 } else {
2125 result.push(c);
2126 }
2127 }
2128 result
2129}
2130
2131#[cfg(test)]
2132mod tests {
2133 use super::*;
2134
2135 #[test]
2136 fn snake_case_input_passes_through_unchanged() {
2137 assert_eq!(camel_to_snake_case("is_vip"), "is_vip");
2138 assert_eq!(camel_to_snake_case("flag_reason"), "flag_reason");
2139 assert_eq!(camel_to_snake_case("loyalty_tier_v2"), "loyalty_tier_v2");
2140 }
2141
2142 #[test]
2143 fn single_word_unchanged() {
2144 assert_eq!(camel_to_snake_case("name"), "name");
2145 assert_eq!(camel_to_snake_case("id"), "id");
2146 assert_eq!(camel_to_snake_case(""), "");
2147 }
2148
2149 #[test]
2150 fn camel_case_converts() {
2151 assert_eq!(camel_to_snake_case("isVip"), "is_vip");
2152 assert_eq!(camel_to_snake_case("userId"), "user_id");
2153 assert_eq!(camel_to_snake_case("customerSegment"), "customer_segment");
2154 assert_eq!(camel_to_snake_case("blacklistReason"), "blacklist_reason");
2155 }
2156
2157 #[test]
2158 fn pascal_case_converts() {
2159 assert_eq!(camel_to_snake_case("IsVip"), "is_vip");
2160 assert_eq!(camel_to_snake_case("UserId"), "user_id");
2161 }
2162
2163 #[test]
2164 fn acronym_runs_stay_together() {
2165 assert_eq!(camel_to_snake_case("IOError"), "io_error");
2168 assert_eq!(camel_to_snake_case("httpURL"), "http_url");
2169 assert_eq!(camel_to_snake_case("ABC"), "abc");
2170 assert_eq!(camel_to_snake_case("XMLHttpRequest"), "xml_http_request");
2171 }
2172
2173 #[test]
2174 fn digits_count_as_lowercase_for_boundary() {
2175 assert_eq!(camel_to_snake_case("v2Field"), "v2_field");
2176 assert_eq!(camel_to_snake_case("field2Name"), "field2_name");
2177 }
2178
2179 #[test]
2180 fn underscores_not_doubled() {
2181 assert_eq!(camel_to_snake_case("_Foo"), "_foo");
2182 assert_eq!(camel_to_snake_case("foo_Bar"), "foo_bar");
2183 }
2184
2185 #[test]
2186 fn shallow_pages_are_allowed() {
2187 assert!(pagination_depth_error(1, 100).is_none());
2189 assert!(pagination_depth_error(101, 100).is_none());
2191 }
2192
2193 #[test]
2194 fn pages_past_the_cap_are_rejected() {
2195 assert!(pagination_depth_error(102, 100).is_some());
2197 assert!(pagination_depth_error(1002, 10).is_some());
2199 }
2200
2201 #[test]
2202 fn oversized_page_size_is_clamped_before_the_check() {
2203 assert!(pagination_depth_error(102, 200).is_some());
2205 assert!(pagination_depth_error(101, 200).is_none());
2207 }
2208
2209 #[test]
2210 fn huge_page_number_does_not_overflow() {
2211 assert!(pagination_depth_error(u32::MAX, u32::MAX).is_some());
2213 }
2214
2215 #[test]
2218 fn batch_size_within_limit_is_allowed() {
2219 assert!(batch_size_error(0).is_none());
2220 assert!(batch_size_error(MAX_BATCH_SIZE).is_none());
2221 }
2222
2223 #[test]
2224 fn batch_size_over_limit_is_rejected() {
2225 assert!(batch_size_error(MAX_BATCH_SIZE + 1).is_some());
2226 }
2227
2228 #[test]
2229 fn bulk_patch_request_parses_shared_shape() {
2230 let json = r#"{ "ids": ["a", "b"], "patch": { "status": "void" } }"#;
2231 let req: BulkPatchRequest = serde_json::from_str(json).unwrap();
2232 let items = req.into_items();
2233 assert_eq!(items.len(), 2);
2234 assert_eq!(items[0].1.get("status").unwrap(), "void");
2236 assert_eq!(items[1].1.get("status").unwrap(), "void");
2237 assert_eq!(items[0].0, "a");
2238 assert_eq!(items[1].0, "b");
2239 }
2240
2241 #[test]
2242 fn bulk_patch_request_parses_per_item_shape() {
2243 let json = r#"{ "items": [
2244 { "id": "a", "patch": { "status": "void" } },
2245 { "id": "b", "patch": { "note": "late" } }
2246 ] }"#;
2247 let req: BulkPatchRequest = serde_json::from_str(json).unwrap();
2248 let items = req.into_items();
2249 assert_eq!(items.len(), 2);
2250 assert_eq!(items[0].0, "a");
2251 assert_eq!(items[0].1.get("status").unwrap(), "void");
2252 assert_eq!(items[1].0, "b");
2253 assert_eq!(items[1].1.get("note").unwrap(), "late");
2254 }
2255
2256 #[test]
2257 fn batch_ids_request_parses() {
2258 let req: BatchIdsRequest = serde_json::from_str(r#"{ "ids": ["x", "y", "z"] }"#).unwrap();
2259 assert_eq!(req.ids, vec!["x", "y", "z"]);
2260 }
2261
2262 fn fields(q: &[(&str, &str)]) -> Vec<String> {
2265 let map: HashMap<String, String> =
2266 q.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect();
2267 sparse_fields(&map)
2268 }
2269
2270 #[test]
2271 fn sparse_fields_parses_comma_list_and_trims() {
2272 assert_eq!(fields(&[("fields", "id, name ,basePrice")]), vec!["id", "name", "basePrice"]);
2273 }
2274
2275 #[test]
2276 fn sparse_fields_absent_or_empty_is_no_projection() {
2277 assert!(fields(&[]).is_empty());
2278 assert!(fields(&[("fields", "")]).is_empty());
2279 assert!(fields(&[("fields", " , ")]).is_empty());
2280 }
2281
2282 #[test]
2283 fn project_keeps_requested_keys_plus_id() {
2284 let v = serde_json::json!({ "id": "1", "name": "n", "basePrice": 10, "hppPerUnit": 5 });
2285 let out = project_sparse(v, &["name".into(), "basePrice".into()]);
2286 assert_eq!(out, serde_json::json!({ "id": "1", "name": "n", "basePrice": 10 }));
2287 }
2288
2289 #[test]
2290 fn project_always_includes_id_even_if_not_requested() {
2291 let v = serde_json::json!({ "id": "1", "name": "n" });
2292 let out = project_sparse(v, &["name".into()]);
2293 assert_eq!(out, serde_json::json!({ "id": "1", "name": "n" }));
2294 }
2295
2296 #[test]
2297 fn project_ignores_unknown_keys() {
2298 let v = serde_json::json!({ "id": "1", "name": "n" });
2299 let out = project_sparse(v, &["name".into(), "nope".into()]);
2300 assert_eq!(out, serde_json::json!({ "id": "1", "name": "n" }));
2301 }
2302
2303 #[test]
2304 fn project_empty_fields_returns_full_object() {
2305 let v = serde_json::json!({ "id": "1", "name": "n", "x": 2 });
2306 let out = project_sparse(v.clone(), &[]);
2307 assert_eq!(out, v);
2308 }
2309
2310 #[test]
2311 fn project_non_object_returned_unchanged() {
2312 let v = serde_json::json!("scalar");
2313 assert_eq!(project_sparse(v.clone(), &["name".into()]), v);
2314 }
2315
2316 fn owner_a() -> uuid::Uuid {
2321 uuid::Uuid::parse_str("11111111-1111-4111-8111-111111111111").unwrap()
2322 }
2323 fn owner_b() -> uuid::Uuid {
2324 uuid::Uuid::parse_str("22222222-2222-4222-8222-222222222222").unwrap()
2325 }
2326
2327 fn row() -> serde_json::Value {
2328 serde_json::json!({ "id": "1", "providerId": owner_a().to_string(), "name": "n", "hppPerUnit": 5 })
2329 }
2330 const PRIV: &[&str] = &["hppPerUnit"];
2331
2332 #[test]
2333 fn security_no_private_fields_is_noop() {
2334 let v = row();
2335 assert_eq!(apply_field_security(v.clone(), None, &[], Some("providerId")), v);
2336 }
2337
2338 #[test]
2339 fn security_platform_sees_private() {
2340 let out = apply_field_security(row(), Some(&AccessScope::Platform), PRIV, Some("providerId"));
2341 assert!(out.get("hppPerUnit").is_some());
2342 }
2343
2344 #[test]
2345 fn security_owner_tenant_sees_private() {
2346 let out = apply_field_security(
2347 row(),
2348 Some(&AccessScope::Company(owner_a())),
2349 PRIV,
2350 Some("providerId"),
2351 );
2352 assert!(out.get("hppPerUnit").is_some());
2353 }
2354
2355 #[test]
2356 fn security_other_tenant_stripped() {
2357 let out = apply_field_security(
2358 row(),
2359 Some(&AccessScope::Company(owner_b())),
2360 PRIV,
2361 Some("providerId"),
2362 );
2363 assert!(out.get("hppPerUnit").is_none());
2364 assert!(out.get("name").is_some());
2365 }
2366
2367 #[test]
2368 fn security_absent_scope_fails_closed() {
2369 let out = apply_field_security(row(), None, PRIV, Some("providerId"));
2370 assert!(out.get("hppPerUnit").is_none());
2371 }
2372
2373 #[test]
2376 fn include_relations_parses_include_and_with() {
2377 let mut q = HashMap::new();
2378 q.insert("include".to_string(), "provider, outlet ".to_string());
2379 assert_eq!(include_relations(&q), vec!["provider", "outlet"]);
2380 let mut q2 = HashMap::new();
2381 q2.insert("with".to_string(), "category".to_string());
2382 assert_eq!(include_relations(&q2), vec!["category"]);
2383 assert!(include_relations(&HashMap::new()).is_empty());
2384 }
2385
2386 #[test]
2387 fn snake_to_camel_converts() {
2388 assert_eq!(snake_to_camel("provider_id"), "providerId");
2389 assert_eq!(snake_to_camel("business_name"), "businessName");
2390 assert_eq!(snake_to_camel("id"), "id");
2391 }
2392
2393 #[test]
2394 fn camelize_keys_top_level_only() {
2395 let v = serde_json::json!({ "provider_id": "1", "meta_data": { "created_at": "x" } });
2396 let out = camelize_keys(v);
2397 assert!(out.get("providerId").is_some());
2398 assert!(out.get("metaData").unwrap().get("created_at").is_some());
2400 }
2401
2402 #[test]
2403 fn security_null_owner_only_platform_sees() {
2404 let v = serde_json::json!({ "id": "1", "providerId": null, "hppPerUnit": 5 });
2405 let tenant = apply_field_security(v.clone(), Some(&AccessScope::Company(owner_a())), PRIV, Some("providerId"));
2406 assert!(tenant.get("hppPerUnit").is_none());
2407 let plat = apply_field_security(v, Some(&AccessScope::Platform), PRIV, Some("providerId"));
2408 assert!(plat.get("hppPerUnit").is_some());
2409 }
2410
2411 #[test]
2412 fn a_write_refused_for_named_reasons_answers_422_with_its_violations() {
2413 let v = vec![crate::violation::Violation::new("status", "field_not_writable", "`status` changes only through its verbs")];
2414 let (status, axum::Json(body)) =
2415 write_error::<()>(Some(v), &"validation failed: `status` changes only through its verbs", axum::http::StatusCode::BAD_REQUEST);
2416 assert_eq!(status, axum::http::StatusCode::UNPROCESSABLE_ENTITY);
2417 let json = serde_json::to_value(&body).unwrap();
2418 assert_eq!(json["error"], "validation failed: `status` changes only through its verbs");
2419 assert_eq!(json["violations"][0]["path"], "status");
2420 assert_eq!(json["violations"][0]["code"], "field_not_writable");
2421
2422 let (status, axum::Json(body)) = write_error::<()>(None, &"boom", axum::http::StatusCode::BAD_REQUEST);
2424 assert_eq!(status, axum::http::StatusCode::BAD_REQUEST);
2425 assert!(serde_json::to_value(&body).unwrap().get("violations").is_none());
2426 }
2427}