Skip to main content

backbone_catalog/infrastructure/persistence/
brand_repository.rs

1//! Repository for Brand entities
2//!
3//! Originally generated by metaphor-schema; now **user-owned** — this exact path is declared under
4//! `user_owned` in `metaphor.codegen.yaml`, so the generator skips it wholesale. The custom methods
5//! below hold the catalog write service's brand SQL (4-layer rule: services orchestrate, repos hold
6//! SQL).
7//!
8//! Tenant-agnostic (ADR-0029): no statement here names a tenancy column. When the composing
9//! host mounts a request-scoped org fence, plain pool reads ride the request-dedicated
10//! scoped connection and see only in-scope rows; undecorated, they see the whole table.
11//!
12//! Thin newtype over `backbone_orm::GenericCrudRepository<Brand, backbone_orm::SoftDelete>`.
13//! All standard CRUD methods are available via `Deref`.
14
15use sqlx::PgPool;
16use uuid::Uuid;
17
18use crate::domain::entity::Brand;
19
20/// Table name for Brand entities
21pub const TABLE_NAME: &str = "catalog.brands";
22
23/// Repository for Brand entities.
24///
25/// All standard CRUD, soft-delete, pagination, and bulk methods are
26/// provided automatically via `Deref` to `backbone_orm::GenericCrudRepository`.
27pub struct BrandRepository(
28    backbone_orm::GenericCrudRepository<Brand, backbone_orm::SoftDelete>,
29);
30
31impl std::ops::Deref for BrandRepository {
32    type Target = backbone_orm::GenericCrudRepository<Brand, backbone_orm::SoftDelete>;
33    fn deref(&self) -> &Self::Target { &self.0 }
34}
35
36impl BrandRepository {
37    /// Create a new repository instance.
38    pub fn new(pool: PgPool) -> Self {
39        Self(backbone_orm::GenericCrudRepository::new(pool, TABLE_NAME))
40    }
41}
42
43/// The exact row a validated brand insert writes.
44pub struct NewBrandRow<'a> {
45    pub id: Uuid,
46    pub code: &'a str,
47    pub name: &'a str,
48    pub short_description: Option<&'a str>,
49    pub description: Option<&'a str>,
50    pub logo_url: Option<&'a str>,
51    pub sort_order: i32,
52}
53
54/// Catalog brand SQL. Lives here (not in the service) per the module's 4-layer rule.
55impl BrandRepository {
56    /// `EXISTS` probe for a live row (replaces the prior string-built `exists_in` helper
57    /// in the write service). Used for optional `brand_id` FK validation on create-item.
58    pub async fn exists_id(&self, executor: impl sqlx::Executor<'_, Database = sqlx::Postgres>, id: Uuid) -> Result<bool, sqlx::Error> {
59        let found: Option<Uuid> = sqlx::query_scalar(
60            "SELECT id FROM catalog.brands \
61             WHERE id = $1 AND (metadata->>'deleted_at') IS NULL",
62        )
63        .bind(id)
64        .fetch_optional(executor)
65        .await?;
66        Ok(found.is_some())
67    }
68
69    /// Insert a validated brand row. Unique-constraint errors propagate as `sqlx::Error`
70    /// so the service can disambiguate code duplicates.
71    pub async fn insert_brand(
72        &self,
73        executor: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
74        r: &NewBrandRow<'_>,
75    ) -> Result<(), sqlx::Error> {
76        sqlx::query(
77            r#"INSERT INTO catalog.brands
78                (id, code, name, short_description, description, logo_url, sort_order, status)
79               VALUES ($1,$2,$3,$4,$5,$6,$7,'active'::catalog_status)"#,
80        )
81        .bind(r.id)
82        .bind(r.code)
83        .bind(r.name)
84        .bind(r.short_description)
85        .bind(r.description)
86        .bind(r.logo_url)
87        .bind(r.sort_order)
88        .execute(executor)
89        .await?;
90        Ok(())
91    }
92}
93
94backbone_core::impl_crud_repository!(BrandRepository, Brand, soft_delete);