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}
29
30impl<T> ApiResponse<T> {
31 pub fn success(data: T, message: Option<String>) -> Self {
33 Self {
34 success: true,
35 data: Some(data),
36 message,
37 error: None,
38 }
39 }
40
41 pub fn ok(data: T) -> Self {
43 Self {
44 success: true,
45 data: Some(data),
46 message: None,
47 error: None,
48 }
49 }
50
51 pub fn success_with_message(data: T, message: impl Into<String>) -> Self {
52 Self {
53 success: true,
54 data: Some(data),
55 message: Some(message.into()),
56 error: None,
57 }
58 }
59
60 pub fn error(error: impl Into<String>) -> Self {
61 Self {
62 success: false,
63 data: None,
64 message: None,
65 error: Some(error.into()),
66 }
67 }
68
69 pub fn not_found(entity: &str, id: &str) -> Self {
70 Self {
71 success: false,
72 data: None,
73 message: None,
74 error: Some(format!("{} with id '{}' not found", entity, id)),
75 }
76 }
77}
78
79#[derive(Debug, Deserialize, Default, Clone)]
81#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
82pub struct ListQueryParams {
83 #[serde(default = "default_page")]
84 pub page: u32,
85 #[serde(default = "default_limit")]
86 pub limit: u32,
87 #[serde(default)]
88 pub sort_by: Option<String>,
89 #[serde(default)]
90 pub sort_order: Option<String>,
91 #[serde(default)]
92 pub search: Option<String>,
93 #[serde(default)]
94 pub status: Option<String>,
95 #[serde(flatten)]
96 pub filters: HashMap<String, String>,
97}
98
99fn default_page() -> u32 { 1 }
100fn default_limit() -> u32 { 20 }
101
102const RESERVED_QUERY_KEYS: [&str; 3] = ["fields", "include", "with"];
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct HistoryEntry {
120 pub occurred_at: String,
121 pub action: String,
123 pub actor: String,
124 pub changed: serde_json::Value,
125 pub reason: Option<String>,
126 pub correlation_id: Option<String>,
127}
128
129#[async_trait::async_trait]
136pub trait HistoryProvider: Send + Sync {
137 async fn history(
145 &self,
146 table: &str,
147 id: &str,
148 limit: u32,
149 offset: u32,
150 ) -> Result<Option<Vec<HistoryEntry>>, String>;
151}
152
153const AGGREGATE_QUERY_KEYS: [&str; 7] =
159 ["group_by", "sum", "avg", "min", "max", "group_limit", "group_label"];
160
161fn aggregate_spec(params: &ListQueryParams) -> backbone_orm::repository::AggregateSpec {
167 use backbone_orm::repository::{AggregateFn, AggregateSpec};
168
169 let mut reductions = Vec::new();
170 for (key, func) in [
171 ("sum", AggregateFn::Sum),
172 ("avg", AggregateFn::Avg),
173 ("min", AggregateFn::Min),
174 ("max", AggregateFn::Max),
175 ] {
176 if let Some(raw) = params.filters.get(key) {
177 for field in raw.split(',').map(str::trim).filter(|f| !f.is_empty()) {
178 reductions.push((func, field.to_string()));
179 }
180 }
181 }
182
183 AggregateSpec {
184 group_by: params
185 .filters
186 .get("group_by")
187 .map(|g| g.trim().to_string())
188 .filter(|g| !g.is_empty()),
189 reductions,
190 group_limit: params
191 .filters
192 .get("group_limit")
193 .and_then(|l| l.trim().parse::<usize>().ok())
194 .unwrap_or(0),
195 label_field: params
196 .filters
197 .get("group_label")
198 .map(|l| l.trim().to_string())
199 .filter(|l| !l.is_empty()),
200 label_relation: None,
201 }
202}
203
204fn repository_filters(params: &ListQueryParams) -> HashMap<String, String> {
215 let mut filters = params.filters.clone();
216 for key in RESERVED_QUERY_KEYS {
217 filters.remove(key);
218 }
219 if let Some(search) = params.search.clone() {
220 filters.insert("search".to_string(), search);
221 }
222 if let Some(status) = params.status.clone() {
223 filters.insert("status".to_string(), status);
224 }
225 filters
226}
227
228fn aggregate_filters(params: &ListQueryParams) -> HashMap<String, String> {
231 let mut filters = repository_filters(params);
232 for key in AGGREGATE_QUERY_KEYS {
233 filters.remove(key);
234 }
235 filters
236}
237
238fn sparse_fields(query: &HashMap<String, String>) -> Vec<String> {
239 query
240 .get("fields")
241 .map(|s| {
242 s.split(',')
243 .map(str::trim)
244 .filter(|f| !f.is_empty())
245 .map(str::to_string)
246 .collect()
247 })
248 .unwrap_or_default()
249}
250
251fn to_response_value<R: Serialize>(r: R) -> serde_json::Value {
254 serde_json::to_value(r).unwrap_or(serde_json::Value::Null)
255}
256
257fn project_sparse(mut value: serde_json::Value, fields: &[String]) -> serde_json::Value {
261 if fields.is_empty() {
262 return value;
263 }
264 if let serde_json::Value::Object(map) = &mut value {
265 map.retain(|k, _| k == "id" || fields.iter().any(|f| f == k));
266 }
267 value
268}
269
270#[derive(Debug, Clone, Copy, PartialEq, Eq)]
293pub enum AccessScope {
294 Platform,
296 Company(uuid::Uuid),
298}
299
300impl AccessScope {
301 pub fn company(&self) -> Option<uuid::Uuid> {
307 match self {
308 AccessScope::Platform => None,
309 AccessScope::Company(id) => Some(*id),
310 }
311 }
312}
313
314fn apply_field_security(
320 mut value: serde_json::Value,
321 scope: Option<&AccessScope>,
322 private_fields: &[&str],
323 owner_field: Option<&str>,
324) -> serde_json::Value {
325 if private_fields.is_empty() {
326 return value;
327 }
328 let can_see_private = match scope {
329 Some(AccessScope::Platform) => true,
330 Some(AccessScope::Company(id)) => owner_field
331 .and_then(|f| value.get(f))
332 .and_then(|v| v.as_str())
333 .and_then(|owner| uuid::Uuid::parse_str(owner).ok())
337 .is_some_and(|owner| owner == *id),
338 None => false,
339 };
340 if !can_see_private {
341 if let serde_json::Value::Object(map) = &mut value {
342 for f in private_fields {
343 map.remove(*f);
344 }
345 }
346 }
347 value
348}
349
350fn include_relations(query: &HashMap<String, String>) -> Vec<String> {
352 query
353 .get("include")
354 .or_else(|| query.get("with"))
355 .map(|s| {
356 s.split(',')
357 .map(str::trim)
358 .filter(|s| !s.is_empty())
359 .map(str::to_string)
360 .collect()
361 })
362 .unwrap_or_default()
363}
364
365fn snake_to_camel(s: &str) -> String {
366 let mut out = String::with_capacity(s.len());
367 let mut upper = false;
368 for c in s.chars() {
369 if c == '_' {
370 upper = true;
371 } else if upper {
372 out.extend(c.to_uppercase());
373 upper = false;
374 } else {
375 out.push(c);
376 }
377 }
378 out
379}
380
381fn camelize_keys(v: serde_json::Value) -> serde_json::Value {
384 match v {
385 serde_json::Value::Object(m) => serde_json::Value::Object(
386 m.into_iter().map(|(k, val)| (snake_to_camel(&k), val)).collect(),
387 ),
388 other => other,
389 }
390}
391
392async fn expand_includes<S, E, C, U>(
402 service: &S,
403 rows: &mut [serde_json::Value],
404 includes: &[String],
405) where
406 S: CrudService<E, C, U>,
407 E: backbone_orm::EntityRepoMeta + Send + Sync + 'static,
408 C: Send + Sync + 'static,
409 U: Send + Sync + 'static,
410{
411 if includes.is_empty() || rows.is_empty() {
412 return;
413 }
414 for (rel_name, table, fk_field) in E::relations() {
415 if !includes.iter().any(|i| i == rel_name) {
416 continue;
417 }
418 let mut ids: Vec<String> = rows
419 .iter()
420 .filter_map(|r| r.get(fk_field).and_then(|v| v.as_str()).map(str::to_string))
421 .collect();
422 ids.sort();
423 ids.dedup();
424 if ids.is_empty() {
425 continue;
426 }
427 let related = service.fetch_related_json(table, &ids).await;
428 let mut by_id: HashMap<String, serde_json::Value> = HashMap::new();
429 for obj in related {
430 if let Some(id) = obj.get("id").and_then(|v| v.as_str()).map(str::to_string) {
431 by_id.insert(id, camelize_keys(obj));
432 }
433 }
434 for r in rows.iter_mut() {
435 let related_obj = r
436 .get(fk_field)
437 .and_then(|v| v.as_str())
438 .and_then(|id| by_id.get(id).cloned())
439 .unwrap_or(serde_json::Value::Null);
440 if let serde_json::Value::Object(m) = r {
441 m.insert((*rel_name).to_string(), related_obj);
442 }
443 }
444 }
445}
446
447pub const MAX_PER_PAGE: u32 = 100;
451
452pub const MAX_PAGINATION_OFFSET: u32 = 10_000;
457
458fn pagination_depth_error(page: u32, limit: u32) -> Option<String> {
464 let effective_limit = limit.clamp(1, MAX_PER_PAGE);
465 let offset = page.max(1).saturating_sub(1).saturating_mul(effective_limit);
466 if offset > MAX_PAGINATION_OFFSET {
467 Some(format!(
468 "Result set too deep: offset {offset} exceeds the maximum of \
469 {MAX_PAGINATION_OFFSET}. Please add filters to narrow your search."
470 ))
471 } else {
472 None
473 }
474}
475
476pub use crate::service::MAX_BATCH_SIZE;
482
483fn batch_size_error(count: usize) -> Option<String> {
486 if count > MAX_BATCH_SIZE {
487 Some(format!(
488 "Batch too large: {count} items exceeds the maximum of {MAX_BATCH_SIZE}."
489 ))
490 } else {
491 None
492 }
493}
494
495fn is_bad_query_error(msg: &str) -> bool {
504 let m = msg.to_lowercase();
505 m.contains("does not exist")
506 || m.contains("invalid input syntax")
507 || m.contains("42703")
508 || m.contains("not a column of this entity")
512 || m.contains("needs a numeric column")
513}
514
515#[derive(Debug, Serialize, Deserialize, Clone)]
517#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
518pub struct PaginationResponse {
519 pub total: u64,
520 pub page: u32,
521 pub limit: u32,
522 pub total_pages: u32,
523 #[serde(skip_serializing_if = "Option::is_none")]
527 pub next_cursor: Option<String>,
528 #[serde(skip_serializing_if = "Option::is_none")]
530 pub prev_cursor: Option<String>,
531 #[serde(skip_serializing_if = "Option::is_none")]
533 pub has_more: Option<bool>,
534}
535
536impl PaginationResponse {
537 pub fn new(total: u64, page: u32, limit: u32) -> Self {
538 let total_pages = if limit == 0 { 0 } else { ((total as f64) / (limit as f64)).ceil() as u32 };
539 Self { total, page, limit, total_pages, next_cursor: None, prev_cursor: None, has_more: None }
540 }
541
542 pub fn from_info(info: &backbone_orm::repository::PaginationInfo) -> Self {
546 Self {
547 total: info.total,
548 page: info.page,
549 limit: info.per_page,
550 total_pages: info.total_pages,
551 next_cursor: info.next_cursor.clone(),
552 prev_cursor: info.prev_cursor.clone(),
553 has_more: info.has_more,
554 }
555 }
556}
557
558#[derive(Debug, Serialize)]
560#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
561pub struct PaginatedResponse<T> {
562 pub data: Vec<T>,
563 pub meta: PaginationResponse,
564}
565
566#[derive(Debug, Serialize)]
569#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
570pub struct PaginatedApiResponse<T> {
571 pub success: bool,
572 pub data: Vec<T>,
573 pub meta: PaginationResponse,
574 #[serde(skip_serializing_if = "Option::is_none")]
575 pub error: Option<String>,
576}
577
578impl<T> PaginatedApiResponse<T> {
579 pub fn ok_with_info(data: Vec<T>, info: &backbone_orm::repository::PaginationInfo) -> Self {
582 Self {
583 success: true,
584 data,
585 meta: PaginationResponse::from_info(info),
586 error: None,
587 }
588 }
589
590 pub fn ok(data: Vec<T>, total: u64, page: u32, limit: u32) -> Self {
592 Self {
593 success: true,
594 data,
595 meta: PaginationResponse::new(total, page, limit),
596 error: None,
597 }
598 }
599
600 pub fn from_paginated(resp: PaginatedResponse<T>) -> Self {
602 Self {
603 success: true,
604 data: resp.data,
605 meta: resp.meta,
606 error: None,
607 }
608 }
609
610 pub fn error(error: impl Into<String>) -> Self {
612 Self {
613 success: false,
614 data: Vec::new(),
615 meta: PaginationResponse::new(0, 0, 0),
616 error: Some(error.into()),
617 }
618 }
619}
620
621#[derive(Debug, Serialize, Deserialize)]
623#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
624pub struct BulkCreateRequest<T> {
625 pub items: Vec<T>,
626}
627
628#[derive(Debug, Serialize, Deserialize)]
630#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
631pub struct BulkResponse<T> {
632 pub items: Vec<T>,
633 pub total: usize,
634 pub failed: usize,
635 pub errors: Vec<String>,
636}
637
638#[derive(Debug, Serialize, Deserialize)]
640#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
641pub struct UpsertRequest<T> {
642 pub entity: T,
643 pub create_if_not_exists: bool,
644}
645
646#[derive(Debug, Serialize, Deserialize)]
649#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
650pub struct BatchIdsRequest {
651 pub ids: Vec<String>,
652}
653
654#[derive(Debug, Deserialize)]
657#[serde(bound = "U: DeserializeOwned")]
658#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
659pub struct BulkUpdateItem<U> {
660 pub id: String,
661 #[serde(flatten)]
665 #[cfg_attr(feature = "openapi", schema(value_type = Object))]
666 pub data: U,
667}
668
669#[derive(Debug, Deserialize)]
671#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
672pub struct BulkPatchItem {
673 pub id: String,
674 pub patch: HashMap<String, serde_json::Value>,
675}
676
677#[derive(Debug, Deserialize)]
680#[serde(untagged)]
681#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
682pub enum BulkPatchRequest {
683 Shared {
685 ids: Vec<String>,
686 patch: HashMap<String, serde_json::Value>,
687 },
688 PerItem { items: Vec<BulkPatchItem> },
690}
691
692impl BulkPatchRequest {
693 fn into_items(self) -> Vec<(String, HashMap<String, serde_json::Value>)> {
695 match self {
696 BulkPatchRequest::Shared { ids, patch } => {
697 ids.into_iter().map(|id| (id, patch.clone())).collect()
698 }
699 BulkPatchRequest::PerItem { items } => {
700 items.into_iter().map(|it| (it.id, it.patch)).collect()
701 }
702 }
703 }
704}
705
706#[derive(Debug, Serialize, Deserialize)]
708#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
709pub struct FilterOptions {
710 pub filters: HashMap<String, String>,
711 pub sort_by: Option<String>,
712 pub sort_order: Option<SortOrder>,
713}
714
715#[derive(Debug, Serialize, Deserialize, Clone, Default)]
717#[serde(rename_all = "lowercase")]
718#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
719pub enum SortOrder {
720 #[default]
721 Asc,
722 Desc,
723}
724
725#[derive(Debug, Serialize, Deserialize)]
727#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
728pub struct ListRequest {
729 pub page: Option<u32>,
730 pub limit: Option<u32>,
731 pub sort_by: Option<String>,
732 pub sort_order: Option<SortOrder>,
733 pub filters: Option<HashMap<String, String>>,
734}
735
736impl Default for ListRequest {
737 fn default() -> Self {
738 Self {
739 page: Some(1),
740 limit: Some(20),
741 sort_by: None,
742 sort_order: None,
743 filters: None,
744 }
745 }
746}
747
748#[async_trait::async_trait]
772pub trait CrudService<Entity, CreateDto, UpdateDto>: Send + Sync
773where
774 Entity: Send + Sync + 'static,
777 CreateDto: Send + Sync + 'static,
778 UpdateDto: Send + Sync + 'static,
779{
780 type Error: std::error::Error + Send + Sync;
782
783 fn entity_name() -> &'static str;
785
786 async fn fetch_related_json(
789 &self,
790 _table: &str,
791 _ids: &[String],
792 ) -> Vec<serde_json::Value> {
793 Vec::new()
794 }
795
796 async fn list(&self, page: u32, limit: u32, filters: HashMap<String, String>) -> Result<(Vec<Entity>, u64), Self::Error>;
798
799 async fn list_with_info(
803 &self,
804 page: u32,
805 limit: u32,
806 filters: HashMap<String, String>,
807 ) -> Result<(Vec<Entity>, backbone_orm::repository::PaginationInfo), Self::Error> {
808 let (rows, total) = self.list(page, limit, filters).await?;
809 Ok((
810 rows,
811 backbone_orm::repository::PaginationInfo::new(page, limit, total),
812 ))
813 }
814
815 async fn create(&self, dto: CreateDto) -> Result<Entity, Self::Error>;
817
818 async fn get_by_id(&self, id: &str) -> Result<Option<Entity>, Self::Error>;
820
821 async fn update(&self, id: &str, dto: UpdateDto) -> Result<Option<Entity>, Self::Error>;
823
824 async fn partial_update(&self, id: &str, fields: HashMap<String, serde_json::Value>) -> Result<Option<Entity>, Self::Error>;
826
827 async fn soft_delete(&self, id: &str) -> Result<bool, Self::Error>;
829
830 async fn bulk_create(&self, items: Vec<CreateDto>) -> Result<Vec<Entity>, Self::Error>;
832
833 async fn upsert(&self, dto: CreateDto) -> Result<Entity, Self::Error>;
835
836 async fn list_deleted(&self, page: u32, limit: u32) -> Result<(Vec<Entity>, u64), Self::Error>;
838
839 async fn restore(&self, id: &str) -> Result<Option<Entity>, Self::Error>;
841
842 async fn empty_trash(&self) -> Result<u64, Self::Error>;
844
845 async fn get_deleted_by_id(&self, id: &str) -> Result<Option<Entity>, Self::Error>;
847
848 async fn permanent_delete(&self, id: &str) -> Result<bool, Self::Error>;
850
851 async fn list_deleted_filtered(&self, page: u32, limit: u32, filters: HashMap<String, String>) -> Result<(Vec<Entity>, u64), Self::Error>;
853
854 async fn count_active(&self) -> Result<u64, Self::Error>;
856
857 fn table_name(&self) -> Option<&str> {
864 None
865 }
866
867 async fn aggregate(
872 &self,
873 spec: &backbone_orm::repository::AggregateSpec,
874 filters: HashMap<String, String>,
875 ) -> Result<backbone_orm::repository::AggregateResult, Self::Error>;
876
877 async fn count_active_filtered(
890 &self,
891 filters: HashMap<String, String>,
892 ) -> Result<u64, Self::Error> {
893 self.list(1, 1, filters).await.map(|(_, total)| total)
894 }
895
896 async fn count_deleted(&self) -> Result<u64, Self::Error>;
898
899 async fn bulk_soft_delete(&self, ids: Vec<String>) -> Result<u64, Self::Error> {
908 let mut n = 0;
909 for id in ids {
910 if self.soft_delete(&id).await? {
911 n += 1;
912 }
913 }
914 Ok(n)
915 }
916
917 async fn bulk_restore(&self, ids: Vec<String>) -> Result<Vec<Entity>, Self::Error> {
919 let mut out = Vec::with_capacity(ids.len());
920 for id in ids {
921 if let Some(e) = self.restore(&id).await? {
922 out.push(e);
923 }
924 }
925 Ok(out)
926 }
927
928 async fn bulk_permanent_delete(&self, ids: Vec<String>) -> Result<u64, Self::Error> {
930 let mut n = 0;
931 for id in ids {
932 if self.permanent_delete(&id).await? {
933 n += 1;
934 }
935 }
936 Ok(n)
937 }
938
939 async fn restore_all(&self) -> Result<u64, Self::Error> {
944 Ok(0)
945 }
946
947 async fn bulk_update(&self, items: Vec<(String, UpdateDto)>) -> Result<Vec<Entity>, Self::Error> {
949 let mut out = Vec::with_capacity(items.len());
950 for (id, dto) in items {
951 if let Some(e) = self.update(&id, dto).await? {
952 out.push(e);
953 }
954 }
955 Ok(out)
956 }
957
958 async fn bulk_partial_update(
960 &self,
961 items: Vec<(String, HashMap<String, serde_json::Value>)>,
962 ) -> Result<Vec<Entity>, Self::Error> {
963 let mut out = Vec::with_capacity(items.len());
964 for (id, fields) in items {
965 if let Some(e) = self.partial_update(&id, fields).await? {
966 out.push(e);
967 }
968 }
969 Ok(out)
970 }
971}
972
973pub struct BackboneCrudHandler<S, E, C, U, R>
996where
997 S: CrudService<E, C, U> + 'static,
998 E: Serialize + Send + Sync + 'static,
999 C: DeserializeOwned + Send + Sync + 'static,
1000 U: DeserializeOwned + Send + Sync + 'static,
1001 R: From<E> + Serialize + Send + Sync + 'static,
1002{
1003 service: Arc<S>,
1004 _phantom: std::marker::PhantomData<(E, C, U, R)>,
1005}
1006
1007impl<S, E, C, U, R> BackboneCrudHandler<S, E, C, U, R>
1008where
1009 S: CrudService<E, C, U> + 'static,
1010 E: Serialize + Send + Sync + Clone + backbone_orm::EntityRepoMeta + 'static,
1011 C: DeserializeOwned + Send + Sync + 'static,
1012 U: DeserializeOwned + Send + Sync + 'static,
1013 R: From<E> + Serialize + Send + Sync + 'static,
1014{
1015 pub fn new(service: Arc<S>) -> Self {
1016 Self {
1017 service,
1018 _phantom: std::marker::PhantomData,
1019 }
1020 }
1021
1022 pub fn routes(service: Arc<S>, base_path: &str) -> axum::Router<()>
1046 where
1047 S: Clone,
1048 {
1049 Self::read_routes(service.clone(), base_path)
1050 .merge(Self::write_routes(service, base_path))
1051 }
1052
1053 pub fn read_routes(service: Arc<S>, base_path: &str) -> axum::Router<()>
1067 where
1068 S: Clone,
1069 {
1070 use axum::{
1071 extract::{Path, Query},
1072 routing::get,
1073 Extension, Router,
1074 };
1075
1076 let handler = Arc::new(Self::new(service));
1077
1078 Router::new()
1079 .route(base_path, get({
1081 let h = handler.clone();
1082 move |query: Query<ListQueryParams>, access: Option<Extension<AccessScope>>| async move {
1083 Self::list_handler(h, query, access).await
1084 }
1085 }))
1086 .route(&format!("{}/trash", base_path), get({
1088 let h = handler.clone();
1089 move |query: Query<ListQueryParams>, access: Option<Extension<AccessScope>>| async move {
1090 Self::list_deleted_handler(h, query, access).await
1091 }
1092 }))
1093 .route(&format!("{}/:id", base_path), get({
1095 let h = handler.clone();
1096 move |path: Path<String>, query: Query<ListQueryParams>, access: Option<Extension<AccessScope>>| async move {
1097 Self::get_handler(h, path, query, access).await
1098 }
1099 }))
1100 .route(&format!("{}/:id/deleted", base_path), get({
1102 let h = handler.clone();
1103 move |path: Path<String>, query: Query<ListQueryParams>, access: Option<Extension<AccessScope>>| async move {
1104 Self::get_deleted_handler(h, path, query, access).await
1105 }
1106 }))
1107 .route(&format!("{}/:id/history", base_path), get({
1110 let h = handler.clone();
1111 move |path: axum::extract::Path<String>,
1112 query: axum::extract::Query<ListQueryParams>,
1113 provider: Option<axum::Extension<std::sync::Arc<dyn HistoryProvider>>>| async move {
1114 Self::history_handler(h, path, query, provider).await
1115 }
1116 }))
1117 .route(&format!("{}/aggregate", base_path), get({
1119 let h = handler.clone();
1120 move |query: axum::extract::Query<ListQueryParams>| async move {
1121 Self::aggregate_handler(h, query).await
1122 }
1123 }))
1124 .route(&format!("{}/count", base_path), get({
1125 let h = handler.clone();
1126 move |query: axum::extract::Query<ListQueryParams>| async move {
1127 Self::count_active_handler(h, query).await
1128 }
1129 }))
1130 .route(&format!("{}/trash/count", base_path), get({
1132 let h = handler.clone();
1133 move || async move {
1134 Self::count_deleted_handler(h).await
1135 }
1136 }))
1137 }
1138
1139 pub fn write_routes(service: Arc<S>, base_path: &str) -> axum::Router<()>
1156 where
1157 S: Clone,
1158 {
1159 use axum::{
1160 extract::Path,
1161 routing::{delete, patch, post, put},
1162 Router,
1163 };
1164
1165 let handler = Arc::new(Self::new(service));
1166
1167 Router::new()
1168 .route(base_path, post({
1170 let h = handler.clone();
1171 move |body: JsonOrForm<C>| async move {
1172 Self::create_handler(h, body).await
1173 }
1174 }))
1175 .route(&format!("{}/bulk", base_path), post({
1177 let h = handler.clone();
1178 move |body: JsonOrForm<Vec<C>>| async move {
1179 Self::bulk_create_handler(h, body).await
1180 }
1181 }))
1182 .route(&format!("{}/upsert", base_path), post({
1184 let h = handler.clone();
1185 move |body: JsonOrForm<C>| async move {
1186 Self::upsert_handler(h, body).await
1187 }
1188 }))
1189 .route(&format!("{}/delete/bulk", base_path), post({
1191 let h = handler.clone();
1192 move |body: JsonOrForm<BatchIdsRequest>| async move {
1193 Self::bulk_delete_handler(h, body).await
1194 }
1195 }))
1196 .route(&format!("{}/restore/bulk", base_path), post({
1198 let h = handler.clone();
1199 move |body: JsonOrForm<BatchIdsRequest>| async move {
1200 Self::bulk_restore_handler(h, body).await
1201 }
1202 }))
1203 .route(&format!("{}/restore/all", base_path), post({
1205 let h = handler.clone();
1206 move || async move {
1207 Self::restore_all_handler(h).await
1208 }
1209 }))
1210 .route(&format!("{}/trash/bulk", base_path), delete({
1213 let h = handler.clone();
1214 move |body: JsonOrForm<BatchIdsRequest>| async move {
1215 Self::bulk_permanent_delete_handler(h, body).await
1216 }
1217 }))
1218 .route(&format!("{}/bulk", base_path), put({
1220 let h = handler.clone();
1221 move |body: JsonOrForm<Vec<BulkUpdateItem<U>>>| async move {
1222 Self::bulk_update_handler(h, body).await
1223 }
1224 }))
1225 .route(&format!("{}/bulk", base_path), patch({
1227 let h = handler.clone();
1228 move |body: JsonOrForm<BulkPatchRequest>| async move {
1229 Self::bulk_patch_handler(h, body).await
1230 }
1231 }))
1232 .route(&format!("{}/empty", base_path), delete({
1234 let h = handler.clone();
1235 move || async move {
1236 Self::empty_trash_handler(h).await
1237 }
1238 }))
1239 .route(&format!("{}/trash/:id", base_path), delete({
1241 let h = handler.clone();
1242 move |path: Path<String>| async move {
1243 Self::permanent_delete_handler(h, path).await
1244 }
1245 }))
1246 .route(&format!("{}/:id", base_path), put({
1248 let h = handler.clone();
1249 move |path: Path<String>, body: JsonOrForm<U>| async move {
1250 Self::update_handler(h, path, body).await
1251 }
1252 }))
1253 .route(&format!("{}/:id", base_path), patch({
1255 let h = handler.clone();
1256 move |path: Path<String>, body: JsonOrForm<HashMap<String, serde_json::Value>>| async move {
1257 Self::partial_update_handler(h, path, body).await
1258 }
1259 }))
1260 .route(&format!("{}/:id", base_path), delete({
1262 let h = handler.clone();
1263 move |path: Path<String>| async move {
1264 Self::delete_handler(h, path).await
1265 }
1266 }))
1267 .route(&format!("{}/:id/restore", base_path), post({
1269 let h = handler.clone();
1270 move |path: Path<String>| async move {
1271 Self::restore_handler(h, path).await
1272 }
1273 }))
1274 }
1275
1276 async fn list_handler(
1281 handler: Arc<Self>,
1282 axum::extract::Query(params): axum::extract::Query<ListQueryParams>,
1283 access: Option<axum::Extension<AccessScope>>,
1284 ) -> impl axum::response::IntoResponse {
1285 use axum::{http::StatusCode, Json};
1286
1287 if let Some(err) = pagination_depth_error(params.page, params.limit) {
1288 return (StatusCode::BAD_REQUEST, Json(PaginatedApiResponse::<serde_json::Value>::error(err)));
1289 }
1290
1291 let fields = sparse_fields(¶ms.filters);
1294 let includes = include_relations(¶ms.filters);
1295 let scope = access.map(|axum::Extension(s)| s);
1296
1297 let filters = repository_filters(¶ms);
1298
1299 match handler
1300 .service
1301 .list_with_info(params.page, params.limit, filters)
1302 .await
1303 {
1304 Ok((entities, info)) => {
1305 let mut rows: Vec<serde_json::Value> = entities
1308 .into_iter()
1309 .map(|e| {
1310 apply_field_security(
1311 to_response_value(R::from(e)),
1312 scope.as_ref(),
1313 E::private_fields(),
1314 E::owner_field(),
1315 )
1316 })
1317 .collect();
1318 expand_includes::<S, E, C, U>(&*handler.service, &mut rows, &includes).await;
1319 let items: Vec<serde_json::Value> =
1320 rows.into_iter().map(|r| project_sparse(r, &fields)).collect();
1321 let response = PaginatedApiResponse::ok_with_info(items, &info);
1322 (StatusCode::OK, Json(response))
1323 }
1324 Err(e) => {
1325 let msg = e.to_string();
1326 if is_bad_query_error(&msg) {
1327 (StatusCode::BAD_REQUEST, Json(PaginatedApiResponse::<serde_json::Value>::error(
1328 format!("Invalid query parameter or filter: {msg}"),
1329 )))
1330 } else {
1331 (StatusCode::INTERNAL_SERVER_ERROR, Json(PaginatedApiResponse::<serde_json::Value>::error(msg)))
1332 }
1333 }
1334 }
1335 }
1336
1337 async fn create_handler(
1338 handler: Arc<Self>,
1339 JsonOrForm(dto): JsonOrForm<C>,
1340 ) -> impl axum::response::IntoResponse {
1341 use axum::{http::StatusCode, Json};
1342
1343 match handler.service.create(dto).await {
1344 Ok(entity) => {
1345 let response: R = entity.into();
1346 (StatusCode::CREATED, Json(ApiResponse::ok(response)))
1347 }
1348 Err(e) => {
1349 let error_str = e.to_string();
1350 if error_str.contains("conflict") || error_str.contains("already exists") {
1351 (StatusCode::CONFLICT, Json(ApiResponse::<R>::error(error_str)))
1352 } else {
1353 (StatusCode::BAD_REQUEST, Json(ApiResponse::<R>::error(error_str)))
1354 }
1355 }
1356 }
1357 }
1358
1359 async fn get_handler(
1360 handler: Arc<Self>,
1361 axum::extract::Path(id): axum::extract::Path<String>,
1362 axum::extract::Query(params): axum::extract::Query<ListQueryParams>,
1363 access: Option<axum::Extension<AccessScope>>,
1364 ) -> impl axum::response::IntoResponse {
1365 use axum::{http::StatusCode, Json};
1366
1367 let fields = sparse_fields(¶ms.filters);
1368 let includes = include_relations(¶ms.filters);
1369 let scope = access.map(|axum::Extension(s)| s);
1370
1371 match handler.service.get_by_id(&id).await {
1372 Ok(Some(entity)) => {
1373 let secured = apply_field_security(
1374 to_response_value(R::from(entity)),
1375 scope.as_ref(),
1376 E::private_fields(),
1377 E::owner_field(),
1378 );
1379 let mut rows = [secured];
1380 expand_includes::<S, E, C, U>(&*handler.service, &mut rows, &includes).await;
1381 let [secured] = rows;
1382 let value = project_sparse(secured, &fields);
1383 (StatusCode::OK, Json(ApiResponse::ok(value)))
1384 }
1385 Ok(None) => {
1386 (StatusCode::NOT_FOUND, Json(ApiResponse::<serde_json::Value>::not_found(S::entity_name(), &id)))
1387 }
1388 Err(e) => {
1389 (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<serde_json::Value>::error(e.to_string())))
1390 }
1391 }
1392 }
1393
1394 async fn update_handler(
1395 handler: Arc<Self>,
1396 axum::extract::Path(id): axum::extract::Path<String>,
1397 JsonOrForm(dto): JsonOrForm<U>,
1398 ) -> impl axum::response::IntoResponse {
1399 use axum::{http::StatusCode, Json};
1400
1401 match handler.service.update(&id, dto).await {
1402 Ok(Some(entity)) => {
1403 let response: R = entity.into();
1404 (StatusCode::OK, Json(ApiResponse::ok(response)))
1405 }
1406 Ok(None) => {
1407 (StatusCode::NOT_FOUND, Json(ApiResponse::<R>::not_found(S::entity_name(), &id)))
1408 }
1409 Err(e) => {
1410 (StatusCode::BAD_REQUEST, Json(ApiResponse::<R>::error(e.to_string())))
1411 }
1412 }
1413 }
1414
1415 async fn partial_update_handler(
1416 handler: Arc<Self>,
1417 axum::extract::Path(id): axum::extract::Path<String>,
1418 JsonOrForm(fields): JsonOrForm<HashMap<String, serde_json::Value>>,
1419 ) -> impl axum::response::IntoResponse {
1420 use axum::{http::StatusCode, Json};
1421
1422 let fields: HashMap<String, serde_json::Value> = fields
1433 .into_iter()
1434 .map(|(k, v)| (camel_to_snake_case(&k), v))
1435 .collect();
1436
1437 match handler.service.partial_update(&id, fields).await {
1438 Ok(Some(entity)) => {
1439 let response: R = entity.into();
1440 (StatusCode::OK, Json(ApiResponse::ok(response)))
1441 }
1442 Ok(None) => {
1443 (StatusCode::NOT_FOUND, Json(ApiResponse::<R>::not_found(S::entity_name(), &id)))
1444 }
1445 Err(e) => {
1446 (StatusCode::BAD_REQUEST, Json(ApiResponse::<R>::error(e.to_string())))
1447 }
1448 }
1449 }
1450
1451 async fn delete_handler(
1452 handler: Arc<Self>,
1453 axum::extract::Path(id): axum::extract::Path<String>,
1454 ) -> impl axum::response::IntoResponse {
1455 use axum::{http::StatusCode, Json};
1456
1457 match handler.service.soft_delete(&id).await {
1458 Ok(true) => {
1459 (StatusCode::OK, Json(ApiResponse::<()>::success_with_message((), "Entity deleted successfully")))
1462 }
1463 Ok(false) => {
1464 (StatusCode::NOT_FOUND, Json(ApiResponse::<()>::not_found(S::entity_name(), &id)))
1465 }
1466 Err(e) => {
1467 (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<()>::error(e.to_string())))
1468 }
1469 }
1470 }
1471
1472 async fn bulk_create_handler(
1473 handler: Arc<Self>,
1474 JsonOrForm(items): JsonOrForm<Vec<C>>,
1475 ) -> impl axum::response::IntoResponse {
1476 use axum::{http::StatusCode, Json};
1477
1478 match handler.service.bulk_create(items).await {
1479 Ok(entities) => {
1480 let result_items: Vec<R> = entities.into_iter().map(R::from).collect();
1481 let total = result_items.len();
1482 let response = BulkResponse {
1483 items: result_items,
1484 total,
1485 failed: 0,
1486 errors: vec![],
1487 };
1488 (StatusCode::CREATED, Json(ApiResponse::ok(response)))
1489 }
1490 Err(e) => {
1491 (StatusCode::BAD_REQUEST, Json(ApiResponse::<BulkResponse<R>>::error(e.to_string())))
1492 }
1493 }
1494 }
1495
1496 async fn bulk_delete_handler(
1499 handler: Arc<Self>,
1500 JsonOrForm(req): JsonOrForm<BatchIdsRequest>,
1501 ) -> impl axum::response::IntoResponse {
1502 use axum::{http::StatusCode, Json};
1503
1504 if let Some(err) = batch_size_error(req.ids.len()) {
1505 return (StatusCode::BAD_REQUEST, Json(ApiResponse::<serde_json::Value>::error(err)));
1506 }
1507 match handler.service.bulk_soft_delete(req.ids).await {
1508 Ok(count) => (StatusCode::OK, Json(ApiResponse::success_with_message(
1509 serde_json::json!({ "soft_deleted": count }),
1510 format!("Soft-deleted {count} item(s)"),
1511 ))),
1512 Err(e) => (StatusCode::BAD_REQUEST, Json(ApiResponse::<serde_json::Value>::error(e.to_string()))),
1513 }
1514 }
1515
1516 async fn bulk_restore_handler(
1517 handler: Arc<Self>,
1518 JsonOrForm(req): JsonOrForm<BatchIdsRequest>,
1519 ) -> impl axum::response::IntoResponse {
1520 use axum::{http::StatusCode, Json};
1521
1522 if let Some(err) = batch_size_error(req.ids.len()) {
1523 return (StatusCode::BAD_REQUEST, Json(ApiResponse::<BulkResponse<R>>::error(err)));
1524 }
1525 match handler.service.bulk_restore(req.ids).await {
1526 Ok(entities) => {
1527 let items: Vec<R> = entities.into_iter().map(R::from).collect();
1528 let total = items.len();
1529 (StatusCode::OK, Json(ApiResponse::ok(BulkResponse { items, total, failed: 0, errors: vec![] })))
1530 }
1531 Err(e) => (StatusCode::BAD_REQUEST, Json(ApiResponse::<BulkResponse<R>>::error(e.to_string()))),
1532 }
1533 }
1534
1535 async fn restore_all_handler(
1536 handler: Arc<Self>,
1537 ) -> impl axum::response::IntoResponse {
1538 use axum::{http::StatusCode, Json};
1539
1540 match handler.service.restore_all().await {
1541 Ok(count) => (StatusCode::OK, Json(ApiResponse::success_with_message(
1542 serde_json::json!({ "restored": count }),
1543 format!("Restored {count} item(s) from trash"),
1544 ))),
1545 Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<serde_json::Value>::error(e.to_string()))),
1546 }
1547 }
1548
1549 async fn bulk_permanent_delete_handler(
1550 handler: Arc<Self>,
1551 JsonOrForm(req): JsonOrForm<BatchIdsRequest>,
1552 ) -> impl axum::response::IntoResponse {
1553 use axum::{http::StatusCode, Json};
1554
1555 if let Some(err) = batch_size_error(req.ids.len()) {
1556 return (StatusCode::BAD_REQUEST, Json(ApiResponse::<serde_json::Value>::error(err)));
1557 }
1558 match handler.service.bulk_permanent_delete(req.ids).await {
1559 Ok(count) => (StatusCode::OK, Json(ApiResponse::success_with_message(
1560 serde_json::json!({ "permanently_deleted": count }),
1561 format!("Permanently deleted {count} item(s)"),
1562 ))),
1563 Err(e) => (StatusCode::BAD_REQUEST, Json(ApiResponse::<serde_json::Value>::error(e.to_string()))),
1564 }
1565 }
1566
1567 async fn bulk_update_handler(
1568 handler: Arc<Self>,
1569 JsonOrForm(items): JsonOrForm<Vec<BulkUpdateItem<U>>>,
1570 ) -> impl axum::response::IntoResponse {
1571 use axum::{http::StatusCode, Json};
1572
1573 if let Some(err) = batch_size_error(items.len()) {
1574 return (StatusCode::BAD_REQUEST, Json(ApiResponse::<BulkResponse<R>>::error(err)));
1575 }
1576 let items: Vec<(String, U)> = items.into_iter().map(|it| (it.id, it.data)).collect();
1577 match handler.service.bulk_update(items).await {
1578 Ok(entities) => {
1579 let items: Vec<R> = entities.into_iter().map(R::from).collect();
1580 let total = items.len();
1581 (StatusCode::OK, Json(ApiResponse::ok(BulkResponse { items, total, failed: 0, errors: vec![] })))
1582 }
1583 Err(e) => (StatusCode::BAD_REQUEST, Json(ApiResponse::<BulkResponse<R>>::error(e.to_string()))),
1584 }
1585 }
1586
1587 async fn bulk_patch_handler(
1588 handler: Arc<Self>,
1589 JsonOrForm(req): JsonOrForm<BulkPatchRequest>,
1590 ) -> impl axum::response::IntoResponse {
1591 use axum::{http::StatusCode, Json};
1592
1593 let items = req.into_items();
1594 if let Some(err) = batch_size_error(items.len()) {
1595 return (StatusCode::BAD_REQUEST, Json(ApiResponse::<BulkResponse<R>>::error(err)));
1596 }
1597 let items: Vec<(String, HashMap<String, serde_json::Value>)> = items
1599 .into_iter()
1600 .map(|(id, fields)| {
1601 let fields = fields
1602 .into_iter()
1603 .map(|(k, v)| (camel_to_snake_case(&k), v))
1604 .collect();
1605 (id, fields)
1606 })
1607 .collect();
1608 match handler.service.bulk_partial_update(items).await {
1609 Ok(entities) => {
1610 let items: Vec<R> = entities.into_iter().map(R::from).collect();
1611 let total = items.len();
1612 (StatusCode::OK, Json(ApiResponse::ok(BulkResponse { items, total, failed: 0, errors: vec![] })))
1613 }
1614 Err(e) => (StatusCode::BAD_REQUEST, Json(ApiResponse::<BulkResponse<R>>::error(e.to_string()))),
1615 }
1616 }
1617
1618 async fn upsert_handler(
1619 handler: Arc<Self>,
1620 JsonOrForm(dto): JsonOrForm<C>,
1621 ) -> impl axum::response::IntoResponse {
1622 use axum::{http::StatusCode, Json};
1623
1624 match handler.service.upsert(dto).await {
1625 Ok(entity) => {
1626 let response: R = entity.into();
1627 (StatusCode::OK, Json(ApiResponse::ok(response)))
1628 }
1629 Err(e) => {
1630 (StatusCode::BAD_REQUEST, Json(ApiResponse::<R>::error(e.to_string())))
1631 }
1632 }
1633 }
1634
1635 async fn list_deleted_handler(
1636 handler: Arc<Self>,
1637 axum::extract::Query(params): axum::extract::Query<ListQueryParams>,
1638 access: Option<axum::Extension<AccessScope>>,
1639 ) -> impl axum::response::IntoResponse {
1640 use axum::{http::StatusCode, Json};
1641
1642 if let Some(err) = pagination_depth_error(params.page, params.limit) {
1643 return (StatusCode::BAD_REQUEST, Json(PaginatedApiResponse::<serde_json::Value>::error(err)));
1644 }
1645
1646 let fields = sparse_fields(¶ms.filters);
1647 let includes = include_relations(¶ms.filters);
1648 let scope = access.map(|axum::Extension(s)| s);
1649
1650 match handler.service.list_deleted(params.page, params.limit).await {
1651 Ok((entities, total)) => {
1652 let mut rows: Vec<serde_json::Value> = entities
1656 .into_iter()
1657 .map(|e| {
1658 apply_field_security(
1659 to_response_value(R::from(e)),
1660 scope.as_ref(),
1661 E::private_fields(),
1662 E::owner_field(),
1663 )
1664 })
1665 .collect();
1666 expand_includes::<S, E, C, U>(&*handler.service, &mut rows, &includes).await;
1667 let items: Vec<serde_json::Value> =
1668 rows.into_iter().map(|r| project_sparse(r, &fields)).collect();
1669 let response = PaginatedApiResponse::ok(items, total, params.page, params.limit);
1670 (StatusCode::OK, Json(response))
1671 }
1672 Err(e) => {
1673 let msg = e.to_string();
1674 if is_bad_query_error(&msg) {
1675 (StatusCode::BAD_REQUEST, Json(PaginatedApiResponse::<serde_json::Value>::error(
1676 format!("Invalid query parameter or filter: {msg}"),
1677 )))
1678 } else {
1679 (StatusCode::INTERNAL_SERVER_ERROR, Json(PaginatedApiResponse::<serde_json::Value>::error(msg)))
1680 }
1681 }
1682 }
1683 }
1684
1685 async fn restore_handler(
1686 handler: Arc<Self>,
1687 axum::extract::Path(id): axum::extract::Path<String>,
1688 ) -> impl axum::response::IntoResponse {
1689 use axum::{http::StatusCode, Json};
1690
1691 match handler.service.restore(&id).await {
1692 Ok(Some(entity)) => {
1693 let response: R = entity.into();
1694 (StatusCode::OK, Json(ApiResponse::success_with_message(response, "Entity restored successfully")))
1695 }
1696 Ok(None) => {
1697 (StatusCode::NOT_FOUND, Json(ApiResponse::<R>::not_found(S::entity_name(), &id)))
1698 }
1699 Err(e) => {
1700 (StatusCode::BAD_REQUEST, Json(ApiResponse::<R>::error(e.to_string())))
1701 }
1702 }
1703 }
1704
1705 async fn empty_trash_handler(
1706 handler: Arc<Self>,
1707 ) -> impl axum::response::IntoResponse {
1708 use axum::{http::StatusCode, Json};
1709
1710 match handler.service.empty_trash().await {
1711 Ok(count) => {
1712 (StatusCode::OK, Json(ApiResponse::success_with_message(
1713 serde_json::json!({ "deleted_count": count }),
1714 format!("Successfully deleted {} items from trash", count)
1715 )))
1716 }
1717 Err(e) => {
1718 (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<serde_json::Value>::error(e.to_string())))
1719 }
1720 }
1721 }
1722
1723 async fn get_deleted_handler(
1725 handler: Arc<Self>,
1726 axum::extract::Path(id): axum::extract::Path<String>,
1727 axum::extract::Query(params): axum::extract::Query<ListQueryParams>,
1728 access: Option<axum::Extension<AccessScope>>,
1729 ) -> impl axum::response::IntoResponse {
1730 use axum::{http::StatusCode, Json};
1731
1732 let fields = sparse_fields(¶ms.filters);
1733 let scope = access.map(|axum::Extension(s)| s);
1734
1735 match handler.service.get_deleted_by_id(&id).await {
1736 Ok(Some(entity)) => {
1737 let secured = apply_field_security(
1738 to_response_value(R::from(entity)),
1739 scope.as_ref(),
1740 E::private_fields(),
1741 E::owner_field(),
1742 );
1743 let value = project_sparse(secured, &fields);
1744 (StatusCode::OK, Json(ApiResponse::ok(value)))
1745 }
1746 Ok(None) => {
1747 (StatusCode::NOT_FOUND, Json(ApiResponse::<serde_json::Value>::not_found(&format!("Deleted {}", S::entity_name()), &id)))
1748 }
1749 Err(e) => {
1750 (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<serde_json::Value>::error(e.to_string())))
1751 }
1752 }
1753 }
1754
1755 async fn permanent_delete_handler(
1757 handler: Arc<Self>,
1758 axum::extract::Path(id): axum::extract::Path<String>,
1759 ) -> axum::response::Response {
1760 use axum::{http::StatusCode, Json, response::IntoResponse};
1761
1762 match handler.service.get_deleted_by_id(&id).await {
1764 Ok(Some(_)) => {
1765 match handler.service.permanent_delete(&id).await {
1767 Ok(true) => {
1768 StatusCode::NO_CONTENT.into_response()
1770 }
1771 Ok(false) => {
1772 (StatusCode::NOT_FOUND, Json(ApiResponse::<R>::error(
1773 format!("Failed to permanently delete {}", S::entity_name())
1774 ))).into_response()
1775 }
1776 Err(e) => {
1777 (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<R>::error(e.to_string()))).into_response()
1778 }
1779 }
1780 }
1781 Ok(None) => {
1782 (StatusCode::NOT_FOUND, Json(ApiResponse::<R>::not_found(
1783 &format!("{} in trash", S::entity_name()), &id
1784 ))).into_response()
1785 }
1786 Err(e) => {
1787 (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<R>::error(e.to_string()))).into_response()
1788 }
1789 }
1790 }
1791
1792 async fn count_active_handler(
1794 handler: Arc<Self>,
1795 axum::extract::Query(params): axum::extract::Query<ListQueryParams>,
1796 ) -> impl axum::response::IntoResponse {
1797 use axum::{http::StatusCode, Json};
1798
1799 match handler.service.count_active_filtered(repository_filters(¶ms)).await {
1803 Ok(count) => {
1804 (StatusCode::OK, Json(ApiResponse::ok(serde_json::json!({ "count": count }))))
1805 }
1806 Err(e) => {
1807 let msg = e.to_string();
1810 let code = if is_bad_query_error(&msg) {
1811 StatusCode::BAD_REQUEST
1812 } else {
1813 StatusCode::INTERNAL_SERVER_ERROR
1814 };
1815 (code, Json(ApiResponse::<serde_json::Value>::error(msg)))
1816 }
1817 }
1818 }
1819
1820 async fn aggregate_handler(
1825 handler: Arc<Self>,
1826 axum::extract::Query(params): axum::extract::Query<ListQueryParams>,
1827 ) -> impl axum::response::IntoResponse {
1828 use axum::{http::StatusCode, Json};
1829
1830 let spec = aggregate_spec(¶ms);
1831 match handler.service.aggregate(&spec, aggregate_filters(¶ms)).await {
1832 Ok(result) => {
1833 let render = |g: &backbone_orm::repository::AggregateGroup| {
1834 let mut out = serde_json::Map::new();
1838 out.insert("key".into(), match &g.key {
1839 Some(k) => serde_json::Value::String(k.clone()),
1840 None => serde_json::Value::Null,
1841 });
1842 if g.label.is_some() {
1843 out.insert(
1844 "label".into(),
1845 serde_json::Value::String(g.label.clone().unwrap()),
1846 );
1847 }
1848 out.insert("count".into(), serde_json::json!(g.count));
1849 for (compound, value) in &g.values {
1850 let Some((func, field)) = compound.split_once(':') else { continue };
1851 let slot = out
1852 .entry(func.to_string())
1853 .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
1854 if let Some(obj) = slot.as_object_mut() {
1855 obj.insert(field.to_string(), match value {
1856 Some(v) => serde_json::Value::String(v.clone()),
1860 None => serde_json::Value::Null,
1861 });
1862 }
1863 }
1864 serde_json::Value::Object(out)
1865 };
1866
1867 let body = serde_json::json!({
1868 "groups": result.groups.iter().map(render).collect::<Vec<_>>(),
1869 "total": render(&result.total),
1870 "truncated": result.truncated,
1873 });
1874 (StatusCode::OK, Json(ApiResponse::ok(body)))
1875 }
1876 Err(e) => {
1877 let msg = e.to_string();
1878 let code = if is_bad_query_error(&msg) {
1879 StatusCode::BAD_REQUEST
1880 } else {
1881 StatusCode::INTERNAL_SERVER_ERROR
1882 };
1883 (code, Json(ApiResponse::<serde_json::Value>::error(msg)))
1884 }
1885 }
1886 }
1887
1888 async fn history_handler(
1899 handler: Arc<Self>,
1900 axum::extract::Path(id): axum::extract::Path<String>,
1901 axum::extract::Query(params): axum::extract::Query<ListQueryParams>,
1902 provider: Option<axum::Extension<std::sync::Arc<dyn HistoryProvider>>>,
1903 ) -> impl axum::response::IntoResponse {
1904 use axum::{http::StatusCode, Json};
1905
1906 let Some(axum::Extension(provider)) = provider else {
1907 return (
1908 StatusCode::NOT_IMPLEMENTED,
1909 Json(ApiResponse::<serde_json::Value>::error(
1910 "history is not configured for this service".to_string(),
1911 )),
1912 );
1913 };
1914
1915 let Some(table) = handler.service.table_name().map(|t| t.to_string()) else {
1919 return (
1920 StatusCode::NOT_IMPLEMENTED,
1921 Json(ApiResponse::<serde_json::Value>::error(
1922 "this entity cannot name its table, so its history cannot be keyed".to_string(),
1923 )),
1924 );
1925 };
1926
1927 let limit = params.limit.clamp(1, 200);
1928 let offset = params.page.saturating_sub(1) * limit;
1929
1930 match provider.history(&table, &id, limit, offset).await {
1931 Ok(Some(entries)) => (
1932 StatusCode::OK,
1933 Json(ApiResponse::ok(serde_json::json!({
1934 "audited": true,
1935 "entries": entries,
1936 }))),
1937 ),
1938 Ok(None) => (
1939 StatusCode::OK,
1940 Json(ApiResponse::ok(serde_json::json!({
1941 "audited": false,
1944 "entries": serde_json::Value::Null,
1945 }))),
1946 ),
1947 Err(e) => (
1948 StatusCode::INTERNAL_SERVER_ERROR,
1949 Json(ApiResponse::<serde_json::Value>::error(e)),
1950 ),
1951 }
1952 }
1953
1954 async fn count_deleted_handler(
1956 handler: Arc<Self>,
1957 ) -> impl axum::response::IntoResponse {
1958 use axum::{http::StatusCode, Json};
1959
1960 match handler.service.count_deleted().await {
1961 Ok(count) => {
1962 (StatusCode::OK, Json(ApiResponse::ok(serde_json::json!({ "count": count }))))
1963 }
1964 Err(e) => {
1965 (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::<serde_json::Value>::error(e.to_string())))
1966 }
1967 }
1968 }
1969}
1970
1971pub trait BackboneHttpHandler<T>: Send + Sync {
1996 fn list(&self, request: ListRequest) -> anyhow::Result<ApiResponse<Vec<T>>>;
1998
1999 fn create(&self, request: T) -> anyhow::Result<ApiResponse<T>>;
2001
2002 fn get_by_id(&self, id: &uuid::Uuid) -> anyhow::Result<ApiResponse<T>>;
2004
2005 fn update(&self, id: &uuid::Uuid, request: T) -> anyhow::Result<ApiResponse<T>>;
2007
2008 fn partial_update(&self, id: &uuid::Uuid, fields: HashMap<String, serde_json::Value>) -> anyhow::Result<ApiResponse<T>>;
2010
2011 fn soft_delete(&self, id: &uuid::Uuid) -> anyhow::Result<ApiResponse<()>>;
2013
2014 fn bulk_create(&self, request: BulkCreateRequest<T>) -> anyhow::Result<ApiResponse<BulkResponse<T>>>;
2016
2017 fn upsert(&self, request: UpsertRequest<T>) -> anyhow::Result<ApiResponse<T>>;
2019
2020 fn list_deleted(&self, request: ListRequest) -> anyhow::Result<ApiResponse<Vec<T>>>;
2022
2023 fn restore(&self, id: &uuid::Uuid) -> anyhow::Result<ApiResponse<T>>;
2025
2026 fn empty_trash(&self) -> anyhow::Result<ApiResponse<()>>;
2028
2029 fn get_deleted_by_id(&self, id: &uuid::Uuid) -> anyhow::Result<ApiResponse<T>>;
2031}
2032
2033#[derive(Debug, Serialize, Deserialize)]
2039#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
2040pub struct PaginationRequest {
2041 pub page: u32,
2042 pub limit: u32,
2043 pub sort_by: Option<String>,
2044 pub sort_order: Option<String>,
2045}
2046
2047fn camel_to_snake_case(key: &str) -> String {
2063 let chars: Vec<char> = key.chars().collect();
2064 let mut result = String::with_capacity(key.len() + 2);
2065 for (i, &c) in chars.iter().enumerate() {
2066 if c.is_ascii_uppercase() {
2067 let prev = if i > 0 { chars[i - 1] } else { '\0' };
2068 let next = chars.get(i + 1).copied().unwrap_or('\0');
2069 let crosses_lower = prev.is_ascii_lowercase() || prev.is_ascii_digit();
2073 let crosses_acronym = prev.is_ascii_uppercase() && next.is_ascii_lowercase();
2074 if (crosses_lower || crosses_acronym)
2075 && !result.is_empty()
2076 && !result.ends_with('_')
2077 {
2078 result.push('_');
2079 }
2080 result.push(c.to_ascii_lowercase());
2081 } else {
2082 result.push(c);
2083 }
2084 }
2085 result
2086}
2087
2088#[cfg(test)]
2089mod tests {
2090 use super::*;
2091
2092 #[test]
2093 fn snake_case_input_passes_through_unchanged() {
2094 assert_eq!(camel_to_snake_case("is_vip"), "is_vip");
2095 assert_eq!(camel_to_snake_case("flag_reason"), "flag_reason");
2096 assert_eq!(camel_to_snake_case("loyalty_tier_v2"), "loyalty_tier_v2");
2097 }
2098
2099 #[test]
2100 fn single_word_unchanged() {
2101 assert_eq!(camel_to_snake_case("name"), "name");
2102 assert_eq!(camel_to_snake_case("id"), "id");
2103 assert_eq!(camel_to_snake_case(""), "");
2104 }
2105
2106 #[test]
2107 fn camel_case_converts() {
2108 assert_eq!(camel_to_snake_case("isVip"), "is_vip");
2109 assert_eq!(camel_to_snake_case("userId"), "user_id");
2110 assert_eq!(camel_to_snake_case("customerSegment"), "customer_segment");
2111 assert_eq!(camel_to_snake_case("blacklistReason"), "blacklist_reason");
2112 }
2113
2114 #[test]
2115 fn pascal_case_converts() {
2116 assert_eq!(camel_to_snake_case("IsVip"), "is_vip");
2117 assert_eq!(camel_to_snake_case("UserId"), "user_id");
2118 }
2119
2120 #[test]
2121 fn acronym_runs_stay_together() {
2122 assert_eq!(camel_to_snake_case("IOError"), "io_error");
2125 assert_eq!(camel_to_snake_case("httpURL"), "http_url");
2126 assert_eq!(camel_to_snake_case("ABC"), "abc");
2127 assert_eq!(camel_to_snake_case("XMLHttpRequest"), "xml_http_request");
2128 }
2129
2130 #[test]
2131 fn digits_count_as_lowercase_for_boundary() {
2132 assert_eq!(camel_to_snake_case("v2Field"), "v2_field");
2133 assert_eq!(camel_to_snake_case("field2Name"), "field2_name");
2134 }
2135
2136 #[test]
2137 fn underscores_not_doubled() {
2138 assert_eq!(camel_to_snake_case("_Foo"), "_foo");
2139 assert_eq!(camel_to_snake_case("foo_Bar"), "foo_bar");
2140 }
2141
2142 #[test]
2143 fn shallow_pages_are_allowed() {
2144 assert!(pagination_depth_error(1, 100).is_none());
2146 assert!(pagination_depth_error(101, 100).is_none());
2148 }
2149
2150 #[test]
2151 fn pages_past_the_cap_are_rejected() {
2152 assert!(pagination_depth_error(102, 100).is_some());
2154 assert!(pagination_depth_error(1002, 10).is_some());
2156 }
2157
2158 #[test]
2159 fn oversized_page_size_is_clamped_before_the_check() {
2160 assert!(pagination_depth_error(102, 200).is_some());
2162 assert!(pagination_depth_error(101, 200).is_none());
2164 }
2165
2166 #[test]
2167 fn huge_page_number_does_not_overflow() {
2168 assert!(pagination_depth_error(u32::MAX, u32::MAX).is_some());
2170 }
2171
2172 #[test]
2175 fn batch_size_within_limit_is_allowed() {
2176 assert!(batch_size_error(0).is_none());
2177 assert!(batch_size_error(MAX_BATCH_SIZE).is_none());
2178 }
2179
2180 #[test]
2181 fn batch_size_over_limit_is_rejected() {
2182 assert!(batch_size_error(MAX_BATCH_SIZE + 1).is_some());
2183 }
2184
2185 #[test]
2186 fn bulk_patch_request_parses_shared_shape() {
2187 let json = r#"{ "ids": ["a", "b"], "patch": { "status": "void" } }"#;
2188 let req: BulkPatchRequest = serde_json::from_str(json).unwrap();
2189 let items = req.into_items();
2190 assert_eq!(items.len(), 2);
2191 assert_eq!(items[0].1.get("status").unwrap(), "void");
2193 assert_eq!(items[1].1.get("status").unwrap(), "void");
2194 assert_eq!(items[0].0, "a");
2195 assert_eq!(items[1].0, "b");
2196 }
2197
2198 #[test]
2199 fn bulk_patch_request_parses_per_item_shape() {
2200 let json = r#"{ "items": [
2201 { "id": "a", "patch": { "status": "void" } },
2202 { "id": "b", "patch": { "note": "late" } }
2203 ] }"#;
2204 let req: BulkPatchRequest = serde_json::from_str(json).unwrap();
2205 let items = req.into_items();
2206 assert_eq!(items.len(), 2);
2207 assert_eq!(items[0].0, "a");
2208 assert_eq!(items[0].1.get("status").unwrap(), "void");
2209 assert_eq!(items[1].0, "b");
2210 assert_eq!(items[1].1.get("note").unwrap(), "late");
2211 }
2212
2213 #[test]
2214 fn batch_ids_request_parses() {
2215 let req: BatchIdsRequest = serde_json::from_str(r#"{ "ids": ["x", "y", "z"] }"#).unwrap();
2216 assert_eq!(req.ids, vec!["x", "y", "z"]);
2217 }
2218
2219 fn fields(q: &[(&str, &str)]) -> Vec<String> {
2222 let map: HashMap<String, String> =
2223 q.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect();
2224 sparse_fields(&map)
2225 }
2226
2227 #[test]
2228 fn sparse_fields_parses_comma_list_and_trims() {
2229 assert_eq!(fields(&[("fields", "id, name ,basePrice")]), vec!["id", "name", "basePrice"]);
2230 }
2231
2232 #[test]
2233 fn sparse_fields_absent_or_empty_is_no_projection() {
2234 assert!(fields(&[]).is_empty());
2235 assert!(fields(&[("fields", "")]).is_empty());
2236 assert!(fields(&[("fields", " , ")]).is_empty());
2237 }
2238
2239 #[test]
2240 fn project_keeps_requested_keys_plus_id() {
2241 let v = serde_json::json!({ "id": "1", "name": "n", "basePrice": 10, "hppPerUnit": 5 });
2242 let out = project_sparse(v, &["name".into(), "basePrice".into()]);
2243 assert_eq!(out, serde_json::json!({ "id": "1", "name": "n", "basePrice": 10 }));
2244 }
2245
2246 #[test]
2247 fn project_always_includes_id_even_if_not_requested() {
2248 let v = serde_json::json!({ "id": "1", "name": "n" });
2249 let out = project_sparse(v, &["name".into()]);
2250 assert_eq!(out, serde_json::json!({ "id": "1", "name": "n" }));
2251 }
2252
2253 #[test]
2254 fn project_ignores_unknown_keys() {
2255 let v = serde_json::json!({ "id": "1", "name": "n" });
2256 let out = project_sparse(v, &["name".into(), "nope".into()]);
2257 assert_eq!(out, serde_json::json!({ "id": "1", "name": "n" }));
2258 }
2259
2260 #[test]
2261 fn project_empty_fields_returns_full_object() {
2262 let v = serde_json::json!({ "id": "1", "name": "n", "x": 2 });
2263 let out = project_sparse(v.clone(), &[]);
2264 assert_eq!(out, v);
2265 }
2266
2267 #[test]
2268 fn project_non_object_returned_unchanged() {
2269 let v = serde_json::json!("scalar");
2270 assert_eq!(project_sparse(v.clone(), &["name".into()]), v);
2271 }
2272
2273 fn owner_a() -> uuid::Uuid {
2278 uuid::Uuid::parse_str("11111111-1111-4111-8111-111111111111").unwrap()
2279 }
2280 fn owner_b() -> uuid::Uuid {
2281 uuid::Uuid::parse_str("22222222-2222-4222-8222-222222222222").unwrap()
2282 }
2283
2284 fn row() -> serde_json::Value {
2285 serde_json::json!({ "id": "1", "providerId": owner_a().to_string(), "name": "n", "hppPerUnit": 5 })
2286 }
2287 const PRIV: &[&str] = &["hppPerUnit"];
2288
2289 #[test]
2290 fn security_no_private_fields_is_noop() {
2291 let v = row();
2292 assert_eq!(apply_field_security(v.clone(), None, &[], Some("providerId")), v);
2293 }
2294
2295 #[test]
2296 fn security_platform_sees_private() {
2297 let out = apply_field_security(row(), Some(&AccessScope::Platform), PRIV, Some("providerId"));
2298 assert!(out.get("hppPerUnit").is_some());
2299 }
2300
2301 #[test]
2302 fn security_owner_tenant_sees_private() {
2303 let out = apply_field_security(
2304 row(),
2305 Some(&AccessScope::Company(owner_a())),
2306 PRIV,
2307 Some("providerId"),
2308 );
2309 assert!(out.get("hppPerUnit").is_some());
2310 }
2311
2312 #[test]
2313 fn security_other_tenant_stripped() {
2314 let out = apply_field_security(
2315 row(),
2316 Some(&AccessScope::Company(owner_b())),
2317 PRIV,
2318 Some("providerId"),
2319 );
2320 assert!(out.get("hppPerUnit").is_none());
2321 assert!(out.get("name").is_some());
2322 }
2323
2324 #[test]
2325 fn security_absent_scope_fails_closed() {
2326 let out = apply_field_security(row(), None, PRIV, Some("providerId"));
2327 assert!(out.get("hppPerUnit").is_none());
2328 }
2329
2330 #[test]
2333 fn include_relations_parses_include_and_with() {
2334 let mut q = HashMap::new();
2335 q.insert("include".to_string(), "provider, outlet ".to_string());
2336 assert_eq!(include_relations(&q), vec!["provider", "outlet"]);
2337 let mut q2 = HashMap::new();
2338 q2.insert("with".to_string(), "category".to_string());
2339 assert_eq!(include_relations(&q2), vec!["category"]);
2340 assert!(include_relations(&HashMap::new()).is_empty());
2341 }
2342
2343 #[test]
2344 fn snake_to_camel_converts() {
2345 assert_eq!(snake_to_camel("provider_id"), "providerId");
2346 assert_eq!(snake_to_camel("business_name"), "businessName");
2347 assert_eq!(snake_to_camel("id"), "id");
2348 }
2349
2350 #[test]
2351 fn camelize_keys_top_level_only() {
2352 let v = serde_json::json!({ "provider_id": "1", "meta_data": { "created_at": "x" } });
2353 let out = camelize_keys(v);
2354 assert!(out.get("providerId").is_some());
2355 assert!(out.get("metaData").unwrap().get("created_at").is_some());
2357 }
2358
2359 #[test]
2360 fn security_null_owner_only_platform_sees() {
2361 let v = serde_json::json!({ "id": "1", "providerId": null, "hppPerUnit": 5 });
2362 let tenant = apply_field_security(v.clone(), Some(&AccessScope::Company(owner_a())), PRIV, Some("providerId"));
2363 assert!(tenant.get("hppPerUnit").is_none());
2364 let plat = apply_field_security(v, Some(&AccessScope::Platform), PRIV, Some("providerId"));
2365 assert!(plat.get("hppPerUnit").is_some());
2366 }
2367}