Skip to main content

backbone_catalog/infrastructure/persistence/
attribute_repository.rs

1//! Repository for Attribute 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 attribute SQL (4-layer rule: services orchestrate, repos
6//! hold 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<Attribute, backbone_orm::SoftDelete>`.
13//! All standard CRUD methods are available via `Deref`.
14
15use sqlx::PgPool;
16use uuid::Uuid;
17
18use crate::domain::entity::Attribute;
19
20/// Table name for Attribute entities
21pub const TABLE_NAME: &str = "catalog.attributes";
22
23/// Repository for Attribute entities.
24///
25/// All standard CRUD, soft-delete, pagination, and bulk methods are
26/// provided automatically via `Deref` to `backbone_orm::GenericCrudRepository`.
27pub struct AttributeRepository(
28    backbone_orm::GenericCrudRepository<Attribute, backbone_orm::SoftDelete>,
29);
30
31impl std::ops::Deref for AttributeRepository {
32    type Target = backbone_orm::GenericCrudRepository<Attribute, backbone_orm::SoftDelete>;
33    fn deref(&self) -> &Self::Target { &self.0 }
34}
35
36impl AttributeRepository {
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 attribute insert writes.
44pub struct NewAttributeRow<'a> {
45    pub id: Uuid,
46    pub code: &'a str,
47    pub name: &'a str,
48    pub attribute_type: &'a str,
49}
50
51/// Catalog attribute SQL. Lives here (not in the service) per the module's 4-layer rule.
52impl AttributeRepository {
53    /// `EXISTS` probe for a live row (replaces the prior string-built `exists_in` helper
54    /// in the write service). Used for `attribute_id` FK validation on create-attribute-value.
55    pub async fn exists_id(&self, executor: impl sqlx::Executor<'_, Database = sqlx::Postgres>, id: Uuid) -> Result<bool, sqlx::Error> {
56        let found: Option<Uuid> = sqlx::query_scalar(
57            "SELECT id FROM catalog.attributes \
58             WHERE id = $1 AND (metadata->>'deleted_at') IS NULL",
59        )
60        .bind(id)
61        .fetch_optional(executor)
62        .await?;
63        Ok(found.is_some())
64    }
65
66    /// Resolve an attribute id by code (used by create-item-variant to disambiguate an unknown
67    /// axis from an unknown value).
68    pub async fn find_id_by_code(
69        &self,
70        executor: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
71        code: &str,
72    ) -> Result<Option<Uuid>, sqlx::Error> {
73        let id: Option<Uuid> = sqlx::query_scalar(
74            "SELECT id FROM catalog.attributes \
75             WHERE code=$1 AND (metadata->>'deleted_at') IS NULL",
76        )
77        .bind(code)
78        .fetch_optional(executor)
79        .await?;
80        Ok(id)
81    }
82
83    /// Insert a validated attribute row. Unique-constraint errors propagate as
84    /// `sqlx::Error` so the service can disambiguate code duplicates.
85    pub async fn insert_attribute(
86        &self,
87        executor: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
88        r: &NewAttributeRow<'_>,
89    ) -> Result<(), sqlx::Error> {
90        sqlx::query(
91            r#"INSERT INTO catalog.attributes (id, code, name, attribute_type, status)
92               VALUES ($1,$2,$3,$4::attribute_type,'active'::catalog_status)"#,
93        )
94        .bind(r.id)
95        .bind(r.code)
96        .bind(r.name)
97        .bind(r.attribute_type)
98        .execute(executor)
99        .await?;
100        Ok(())
101    }
102}
103
104backbone_core::impl_crud_repository!(AttributeRepository, Attribute, soft_delete);