Skip to main content

backbone_core/
macros.rs

1//! Code-generation macros for backbone-schema generated repositories.
2//!
3//! These macros centralise the boilerplate `CrudRepository<E>` trait impl so
4//! the generator can emit a single line instead of ~125 lines per entity.
5//!
6//! # Usage (in generated repository files)
7//!
8//! ```rust,ignore
9//! // Entities that use JSONB-based soft delete (metadata.deleted_at):
10//! backbone_core::impl_crud_repository!(UserRepository, User, soft_delete);
11//!
12//! // Entities that use hard deletes only:
13//! backbone_core::impl_crud_repository!(RoleRepository, Role, no_soft_delete);
14//! ```
15
16/// Report an `?include=` hydration failure.
17///
18/// Hydration runs after the main query and only shapes the response, so it does
19/// not fail the request — but it used to return an empty expansion SILENTLY,
20/// which read as "no related rows": every expanded relation rendered as `null`
21/// and the underlying wiring bug was invisible. Logging at error level keeps
22/// the response stable while making the failure diagnosable from service logs.
23pub fn log_include_hydration_failure(table: &str, error: &str) {
24    tracing::error!(
25        relation_table = table,
26        error = error,
27        "include hydration failed; relation expanded as null"
28    );
29}
30
31/// Implement `backbone_core::CrudRepository<E>` for a generated repository struct.
32///
33/// This macro removes ~125 lines of boilerplate per entity by centralising the
34/// `anyhow::Result → RepositoryError` mapping that is otherwise copy-pasted
35/// identically for every entity.
36///
37/// # Method mapping — `soft_delete` variant
38///
39/// | CrudRepository method         | Inherent struct method called       |
40/// |-------------------------------|-------------------------------------|
41/// | `create`                      | `create(&entity)`                   |
42/// | `find_by_id`                  | `find_by_id(id)`                    |
43/// | `find_by_id_including_deleted`| `find_deleted_by_id(id)`            |
44/// | `update`                      | `update(&id, &entity)`              |
45/// | `update_changed`              | `update_changed(&id, &before, &after)` |
46/// | `soft_delete`                 | `soft_delete(id)`                   |
47/// | `restore`                     | `restore(id)`                       |
48/// | `hard_delete`                 | `permanent_delete(id)`              |
49/// | `list`                        | `list_paginated(pagination)`        |
50/// | `list_deleted`                | `list_deleted(pagination)`          |
51/// | `list_deleted_filtered`       | `list_deleted_filtered(pagination, filters)` |
52/// | `count`                       | `count_active()`                    |
53/// | `count_deleted`               | `count_deleted()`                   |
54/// | `bulk_create`                 | `bulk_create(&entities)` (one transaction) |
55/// | `bulk_update_changed`         | `bulk_update_changed(&pairs)` (one transaction) |
56/// | `empty_trash`                 | `empty_trash()`                     |
57///
58/// # Method mapping — `no_soft_delete` variant
59///
60/// | CrudRepository method         | Inherent struct method called       |
61/// |-------------------------------|-------------------------------------|
62/// | `find_by_id_including_deleted`| `find_by_id(id)` (same as normal)   |
63/// | `soft_delete`                 | `delete(id)`                        |
64/// | `restore`                     | `find_by_id(id)` (no-op restore)    |
65/// | `hard_delete`                 | `delete(id)`                        |
66/// | `list_deleted`                | `Ok((vec![], 0))`                   |
67/// | `count`                       | `count()`                           |
68/// | `count_deleted`               | `Ok(0)`                             |
69/// | `empty_trash`                 | `Ok(0)`                             |
70#[macro_export]
71macro_rules! impl_crud_repository {
72    // ── Soft-delete variant ────────────────────────────────────────────────────
73    //
74    // For entities that store deleted_at inside the metadata JSONB column.
75    // The generated struct exposes: soft_delete, restore, permanent_delete,
76    // list_deleted, find_deleted_by_id, count_active, count_deleted, empty_trash.
77    ($repo:ty, $entity:ty, soft_delete) => {
78        #[async_trait::async_trait]
79        impl backbone_core::CrudRepository<$entity> for $repo {
80            async fn fetch_related_json(
81                &self,
82                table: &str,
83                ids: &[String],
84            ) -> Vec<serde_json::Value> {
85                match backbone_orm::fetch_by_ids_as_json(
86                    (&**self).pool(),
87                    (&**self).table_name(),
88                    table,
89                    ids,
90                )
91                .await
92                {
93                    Ok(rows) => rows,
94                    Err(err) => {
95                        backbone_core::log_include_hydration_failure(table, &err.to_string());
96                        Vec::new()
97                    }
98                }
99            }
100
101            // ── NOTE on method resolution ─────────────────────────────────────
102            // This repo is a newtype wrapping `GenericCrudRepository<E, SoftDelete>`
103            // and implements `Deref` to it.  Inside a trait impl, unqualified `self.foo()`
104            // resolves to the *trait* method (recursive) when both the trait and the
105            // inner type have a method with the same name.  We use `(&**self).foo()`
106            // to force resolution through the `Deref` target's inherent methods.
107
108            async fn create(
109                &self,
110                entity: $entity,
111            ) -> Result<$entity, backbone_core::RepositoryError> {
112                (&**self).create(&entity)
113                    .await
114                    .map_err(backbone_core::RepositoryError::from_source)
115            }
116
117            async fn find_by_id(
118                &self,
119                id: &str,
120            ) -> Result<Option<$entity>, backbone_core::RepositoryError> {
121                (&**self).find_by_id(id)
122                    .await
123                    .map_err(backbone_core::RepositoryError::from_source)
124            }
125
126            async fn find_by_id_including_deleted(
127                &self,
128                id: &str,
129            ) -> Result<Option<$entity>, backbone_core::RepositoryError> {
130                (&**self).find_deleted_by_id(id)
131                    .await
132                    .map_err(backbone_core::RepositoryError::from_source)
133            }
134
135            async fn update(
136                &self,
137                entity: $entity,
138            ) -> Result<$entity, backbone_core::RepositoryError> {
139                let id = backbone_core::PersistentEntity::entity_id(&entity);
140                (&**self).update(&id, &entity)
141                    .await
142                    .map_err(backbone_core::RepositoryError::from_source)
143                    .and_then(|opt| opt.ok_or(backbone_core::RepositoryError::NotFound))
144            }
145
146            async fn update_changed(
147                &self,
148                before: &$entity,
149                after: $entity,
150            ) -> Result<Option<$entity>, backbone_core::RepositoryError> {
151                let id = backbone_core::PersistentEntity::entity_id(&after);
152                (&**self)
153                    .update_changed(&id, before, &after)
154                    .await
155                    .map_err(backbone_core::RepositoryError::from_source)
156            }
157
158            async fn soft_delete(
159                &self,
160                id: &str,
161            ) -> Result<bool, backbone_core::RepositoryError> {
162                (&**self).soft_delete(id)
163                    .await
164                    .map_err(backbone_core::RepositoryError::from_source)
165            }
166
167            async fn restore(
168                &self,
169                id: &str,
170            ) -> Result<Option<$entity>, backbone_core::RepositoryError> {
171                (&**self).restore(id)
172                    .await
173                    .map_err(backbone_core::RepositoryError::from_source)
174            }
175
176            async fn hard_delete(
177                &self,
178                id: &str,
179            ) -> Result<bool, backbone_core::RepositoryError> {
180                (&**self).permanent_delete(id)
181                    .await
182                    .map_err(backbone_core::RepositoryError::from_source)
183            }
184
185            async fn list(
186                &self,
187                page: u32,
188                limit: u32,
189            ) -> Result<(Vec<$entity>, u64), backbone_core::RepositoryError> {
190                let pagination =
191                    backbone_orm::repository::PaginationParams { page, per_page: limit };
192                (&**self).list_paginated(pagination)
193                    .await
194                    .map(|r| (r.data, r.pagination.total))
195                    .map_err(backbone_core::RepositoryError::from_source)
196            }
197
198            async fn list_filtered(
199                &self,
200                page: u32,
201                limit: u32,
202                filters: std::collections::HashMap<String, String>,
203            ) -> Result<(Vec<$entity>, u64), backbone_core::RepositoryError> {
204                let pagination =
205                    backbone_orm::repository::PaginationParams { page, per_page: limit };
206                (&**self).list_paginated_filtered(pagination, Some(&filters))
207                    .await
208                    .map(|r| (r.data, r.pagination.total))
209                    .map_err(backbone_core::RepositoryError::from_source)
210            }
211
212            async fn list_filtered_with_info(
213                &self,
214                page: u32,
215                limit: u32,
216                filters: std::collections::HashMap<String, String>,
217            ) -> Result<
218                (Vec<$entity>, backbone_orm::repository::PaginationInfo),
219                backbone_core::RepositoryError,
220            > {
221                let pagination =
222                    backbone_orm::repository::PaginationParams { page, per_page: limit };
223                (&**self)
224                    .list_paginated_filtered(pagination, Some(&filters))
225                    .await
226                    .map(|r| (r.data, r.pagination))
227                    .map_err(backbone_core::RepositoryError::from_source)
228            }
229
230            fn table_name(&self) -> Option<&str> {
231                Some((&**self).table_name())
232            }
233
234            async fn aggregate_filtered(
235                &self,
236                spec: &backbone_orm::repository::AggregateSpec,
237                filters: std::collections::HashMap<String, String>,
238            ) -> Result<backbone_orm::repository::AggregateResult, backbone_core::RepositoryError> {
239                (&**self).aggregate_filtered(spec, Some(&filters))
240                    .await
241                    .map_err(backbone_core::RepositoryError::from_source)
242            }
243
244            async fn list_deleted(
245                &self,
246                page: u32,
247                limit: u32,
248            ) -> Result<(Vec<$entity>, u64), backbone_core::RepositoryError> {
249                let pagination =
250                    backbone_orm::repository::PaginationParams { page, per_page: limit };
251                (&**self).list_deleted(pagination)
252                    .await
253                    .map(|r| (r.data, r.pagination.total))
254                    .map_err(backbone_core::RepositoryError::from_source)
255            }
256
257            async fn list_deleted_filtered(
258                &self,
259                page: u32,
260                limit: u32,
261                filters: std::collections::HashMap<String, String>,
262            ) -> Result<(Vec<$entity>, u64), backbone_core::RepositoryError> {
263                let pagination =
264                    backbone_orm::repository::PaginationParams { page, per_page: limit };
265                (&**self).list_deleted_filtered(pagination, Some(&filters))
266                    .await
267                    .map(|r| (r.data, r.pagination.total))
268                    .map_err(backbone_core::RepositoryError::from_source)
269            }
270
271            async fn count(&self) -> Result<u64, backbone_core::RepositoryError> {
272                (&**self).count_active()
273                    .await
274                    .map_err(backbone_core::RepositoryError::from_source)
275            }
276
277            async fn count_deleted(&self) -> Result<u64, backbone_core::RepositoryError> {
278                (&**self).count_deleted()
279                    .await
280                    .map_err(backbone_core::RepositoryError::from_source)
281            }
282
283            async fn bulk_create(
284                &self,
285                entities: Vec<$entity>,
286            ) -> Result<Vec<$entity>, backbone_core::RepositoryError> {
287                // One transaction: every row or none, the failed row named by its index.
288                (&**self).bulk_create(&entities)
289                    .await
290                    .map_err(backbone_core::RepositoryError::from_batch_source)
291            }
292
293            async fn empty_trash(&self) -> Result<u64, backbone_core::RepositoryError> {
294                (&**self).empty_trash()
295                    .await
296                    .map_err(backbone_core::RepositoryError::from_source)
297            }
298
299            // ── Atomic batch operations (single-transaction, all-or-nothing) ──
300
301            async fn bulk_soft_delete(
302                &self,
303                ids: &[String],
304            ) -> Result<u64, backbone_core::RepositoryError> {
305                (&**self).bulk_soft_delete(ids)
306                    .await
307                    .map_err(backbone_core::RepositoryError::from_source)
308            }
309
310            async fn bulk_restore(
311                &self,
312                ids: &[String],
313            ) -> Result<Vec<$entity>, backbone_core::RepositoryError> {
314                (&**self).bulk_restore(ids)
315                    .await
316                    .map_err(backbone_core::RepositoryError::from_source)
317            }
318
319            async fn bulk_hard_delete(
320                &self,
321                ids: &[String],
322            ) -> Result<u64, backbone_core::RepositoryError> {
323                (&**self).bulk_permanent_delete(ids)
324                    .await
325                    .map_err(backbone_core::RepositoryError::from_source)
326            }
327
328            async fn restore_all(&self) -> Result<Vec<$entity>, backbone_core::RepositoryError> {
329                (&**self).restore_all()
330                    .await
331                    .map_err(backbone_core::RepositoryError::from_source)
332            }
333
334            async fn bulk_update(
335                &self,
336                entities: Vec<$entity>,
337            ) -> Result<Vec<$entity>, backbone_core::RepositoryError> {
338                (&**self).bulk_update(&entities)
339                    .await
340                    .map_err(backbone_core::RepositoryError::from_source)
341            }
342
343            async fn bulk_update_changed(
344                &self,
345                pairs: Vec<($entity, $entity)>,
346            ) -> Result<Vec<$entity>, backbone_core::RepositoryError> {
347                (&**self).bulk_update_changed(&pairs)
348                    .await
349                    .map_err(backbone_core::RepositoryError::from_batch_source)
350            }
351        }
352    };
353
354    // ── No-soft-delete variant ─────────────────────────────────────────────────
355    //
356    // For entities that use hard deletes only (no soft_delete, restore, trash).
357    // Soft-delete-related trait methods return sensible no-op results.
358    ($repo:ty, $entity:ty, no_soft_delete) => {
359        #[async_trait::async_trait]
360        impl backbone_core::CrudRepository<$entity> for $repo {
361            async fn fetch_related_json(
362                &self,
363                table: &str,
364                ids: &[String],
365            ) -> Vec<serde_json::Value> {
366                match backbone_orm::fetch_by_ids_as_json(
367                    (&**self).pool(),
368                    (&**self).table_name(),
369                    table,
370                    ids,
371                )
372                .await
373                {
374                    Ok(rows) => rows,
375                    Err(err) => {
376                        backbone_core::log_include_hydration_failure(table, &err.to_string());
377                        Vec::new()
378                    }
379                }
380            }
381
382            async fn create(
383                &self,
384                entity: $entity,
385            ) -> Result<$entity, backbone_core::RepositoryError> {
386                (&**self).create(&entity)
387                    .await
388                    .map_err(backbone_core::RepositoryError::from_source)
389            }
390
391            async fn find_by_id(
392                &self,
393                id: &str,
394            ) -> Result<Option<$entity>, backbone_core::RepositoryError> {
395                (&**self).find_by_id(id)
396                    .await
397                    .map_err(backbone_core::RepositoryError::from_source)
398            }
399
400            async fn find_by_id_including_deleted(
401                &self,
402                id: &str,
403            ) -> Result<Option<$entity>, backbone_core::RepositoryError> {
404                // No soft delete: "including deleted" == normal lookup
405                (&**self).find_by_id(id)
406                    .await
407                    .map_err(backbone_core::RepositoryError::from_source)
408            }
409
410            async fn update(
411                &self,
412                entity: $entity,
413            ) -> Result<$entity, backbone_core::RepositoryError> {
414                let id = backbone_core::PersistentEntity::entity_id(&entity);
415                (&**self).update(&id, &entity)
416                    .await
417                    .map_err(backbone_core::RepositoryError::from_source)
418                    .and_then(|opt| opt.ok_or(backbone_core::RepositoryError::NotFound))
419            }
420
421            async fn update_changed(
422                &self,
423                before: &$entity,
424                after: $entity,
425            ) -> Result<Option<$entity>, backbone_core::RepositoryError> {
426                let id = backbone_core::PersistentEntity::entity_id(&after);
427                (&**self)
428                    .update_changed(&id, before, &after)
429                    .await
430                    .map_err(backbone_core::RepositoryError::from_source)
431            }
432
433            async fn soft_delete(
434                &self,
435                id: &str,
436            ) -> Result<bool, backbone_core::RepositoryError> {
437                // No soft delete: fall back to hard delete
438                (&**self).delete(id)
439                    .await
440                    .map_err(backbone_core::RepositoryError::from_source)
441            }
442
443            async fn restore(
444                &self,
445                id: &str,
446            ) -> Result<Option<$entity>, backbone_core::RepositoryError> {
447                // No soft delete: "restore" is a no-op, return the current row
448                (&**self).find_by_id(id)
449                    .await
450                    .map_err(backbone_core::RepositoryError::from_source)
451            }
452
453            async fn hard_delete(
454                &self,
455                id: &str,
456            ) -> Result<bool, backbone_core::RepositoryError> {
457                (&**self).delete(id)
458                    .await
459                    .map_err(backbone_core::RepositoryError::from_source)
460            }
461
462            async fn list(
463                &self,
464                page: u32,
465                limit: u32,
466            ) -> Result<(Vec<$entity>, u64), backbone_core::RepositoryError> {
467                let pagination =
468                    backbone_orm::repository::PaginationParams { page, per_page: limit };
469                (&**self).list_paginated(pagination)
470                    .await
471                    .map(|r| (r.data, r.pagination.total))
472                    .map_err(backbone_core::RepositoryError::from_source)
473            }
474
475            async fn list_filtered(
476                &self,
477                page: u32,
478                limit: u32,
479                filters: std::collections::HashMap<String, String>,
480            ) -> Result<(Vec<$entity>, u64), backbone_core::RepositoryError> {
481                let pagination =
482                    backbone_orm::repository::PaginationParams { page, per_page: limit };
483                (&**self).list_paginated_filtered(pagination, Some(&filters))
484                    .await
485                    .map(|r| (r.data, r.pagination.total))
486                    .map_err(backbone_core::RepositoryError::from_source)
487            }
488
489            async fn list_filtered_with_info(
490                &self,
491                page: u32,
492                limit: u32,
493                filters: std::collections::HashMap<String, String>,
494            ) -> Result<
495                (Vec<$entity>, backbone_orm::repository::PaginationInfo),
496                backbone_core::RepositoryError,
497            > {
498                let pagination =
499                    backbone_orm::repository::PaginationParams { page, per_page: limit };
500                (&**self)
501                    .list_paginated_filtered(pagination, Some(&filters))
502                    .await
503                    .map(|r| (r.data, r.pagination))
504                    .map_err(backbone_core::RepositoryError::from_source)
505            }
506
507            fn table_name(&self) -> Option<&str> {
508                Some((&**self).table_name())
509            }
510
511            async fn aggregate_filtered(
512                &self,
513                spec: &backbone_orm::repository::AggregateSpec,
514                filters: std::collections::HashMap<String, String>,
515            ) -> Result<backbone_orm::repository::AggregateResult, backbone_core::RepositoryError> {
516                (&**self).aggregate_filtered(spec, Some(&filters))
517                    .await
518                    .map_err(backbone_core::RepositoryError::from_source)
519            }
520
521            async fn list_deleted(
522                &self,
523                _page: u32,
524                _limit: u32,
525            ) -> Result<(Vec<$entity>, u64), backbone_core::RepositoryError> {
526                // No soft delete: trash is always empty
527                Ok((vec![], 0))
528            }
529
530            async fn list_deleted_filtered(
531                &self,
532                _page: u32,
533                _limit: u32,
534                _filters: std::collections::HashMap<String, String>,
535            ) -> Result<(Vec<$entity>, u64), backbone_core::RepositoryError> {
536                // No soft delete: trash is always empty
537                Ok((vec![], 0))
538            }
539
540            async fn count(&self) -> Result<u64, backbone_core::RepositoryError> {
541                (&**self).count()
542                    .await
543                    .map_err(backbone_core::RepositoryError::from_source)
544            }
545
546            async fn count_deleted(&self) -> Result<u64, backbone_core::RepositoryError> {
547                Ok(0)
548            }
549
550            async fn bulk_create(
551                &self,
552                entities: Vec<$entity>,
553            ) -> Result<Vec<$entity>, backbone_core::RepositoryError> {
554                // One transaction: every row or none, the failed row named by its index.
555                (&**self).bulk_create(&entities)
556                    .await
557                    .map_err(backbone_core::RepositoryError::from_batch_source)
558            }
559
560            async fn empty_trash(&self) -> Result<u64, backbone_core::RepositoryError> {
561                Ok(0)
562            }
563
564            // ── Atomic batch operations ───────────────────────────────────────
565
566            async fn bulk_soft_delete(
567                &self,
568                ids: &[String],
569            ) -> Result<u64, backbone_core::RepositoryError> {
570                // No soft delete: fall back to hard delete
571                (&**self).bulk_delete(ids)
572                    .await
573                    .map_err(backbone_core::RepositoryError::from_source)
574            }
575
576            async fn bulk_restore(
577                &self,
578                _ids: &[String],
579            ) -> Result<Vec<$entity>, backbone_core::RepositoryError> {
580                // No soft delete: nothing is ever in trash to restore
581                Ok(Vec::new())
582            }
583
584            async fn bulk_hard_delete(
585                &self,
586                ids: &[String],
587            ) -> Result<u64, backbone_core::RepositoryError> {
588                (&**self).bulk_delete(ids)
589                    .await
590                    .map_err(backbone_core::RepositoryError::from_source)
591            }
592
593            async fn restore_all(&self) -> Result<Vec<$entity>, backbone_core::RepositoryError> {
594                // No soft delete: trash is always empty
595                Ok(Vec::new())
596            }
597
598            async fn bulk_update(
599                &self,
600                entities: Vec<$entity>,
601            ) -> Result<Vec<$entity>, backbone_core::RepositoryError> {
602                (&**self).bulk_update(&entities)
603                    .await
604                    .map_err(backbone_core::RepositoryError::from_source)
605            }
606
607            async fn bulk_update_changed(
608                &self,
609                pairs: Vec<($entity, $entity)>,
610            ) -> Result<Vec<$entity>, backbone_core::RepositoryError> {
611                (&**self).bulk_update_changed(&pairs)
612                    .await
613                    .map_err(backbone_core::RepositoryError::from_batch_source)
614            }
615        }
616    };
617}