Skip to main content

backbone_catalog/infrastructure/persistence/
attribute_value_repository.rs

1//! Repository for AttributeValue 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-value SQL (4-layer rule: services orchestrate,
6//! repos 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<AttributeValue, backbone_orm::SoftDelete>`.
13//! All standard CRUD methods are available via `Deref`.
14
15use sqlx::PgPool;
16use uuid::Uuid;
17
18use crate::domain::entity::AttributeValue;
19
20/// Table name for AttributeValue entities
21pub const TABLE_NAME: &str = "catalog.attribute_values";
22
23/// Repository for AttributeValue entities.
24///
25/// All standard CRUD, soft-delete, pagination, and bulk methods are
26/// provided automatically via `Deref` to `backbone_orm::GenericCrudRepository`.
27pub struct AttributeValueRepository(
28    backbone_orm::GenericCrudRepository<AttributeValue, backbone_orm::SoftDelete>,
29);
30
31impl std::ops::Deref for AttributeValueRepository {
32    type Target = backbone_orm::GenericCrudRepository<AttributeValue, backbone_orm::SoftDelete>;
33    fn deref(&self) -> &Self::Target { &self.0 }
34}
35
36impl AttributeValueRepository {
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-value insert writes.
44pub struct NewAttributeValueRow<'a> {
45    pub id: Uuid,
46    pub attribute_id: Uuid,
47    pub code: &'a str,
48    pub label: &'a str,
49    pub label_en: Option<&'a str>,
50    pub swatch_hex: Option<&'a str>,
51    pub sort_order: i32,
52}
53
54/// A resolved `(value_id, label)` pair from the attribute_value × attribute join (used by
55/// create-item-variant to validate each option against the registry and to build the variant's
56/// default label).
57#[derive(Debug, Clone)]
58pub struct AttributeValueResolveRow {
59    pub id: Uuid,
60    pub label: String,
61}
62
63/// Catalog attribute-value SQL. Lives here (not in the service) per the module's 4-layer rule.
64impl AttributeValueRepository {
65    /// Resolve `(value_id, label)` for a `(attribute_code, value_code)` pair, joined with the
66    /// attribute registry. Tenant-agnostic: scoped by the caller's connection, never by a
67    /// column here. `None` = the value or its axis is unknown; the caller disambiguates with
68    /// [`AttributeRepository::find_id_by_code`].
69    pub async fn find_value_with_attribute(
70        &self,
71        executor: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
72        attr_code: &str,
73        val_code: &str,
74    ) -> Result<Option<AttributeValueResolveRow>, sqlx::Error> {
75        let row: Option<(Uuid, String)> = sqlx::query_as(
76            r#"SELECT av.id, av.label
77               FROM catalog.attribute_values av
78               JOIN catalog.attributes a ON a.id = av.attribute_id
79               WHERE a.code = $1 AND av.code = $2
80                 AND (a.metadata->>'deleted_at') IS NULL
81                 AND (av.metadata->>'deleted_at') IS NULL"#,
82        )
83        .bind(attr_code)
84        .bind(val_code)
85        .fetch_optional(executor)
86        .await?;
87        Ok(row.map(|(id, label)| AttributeValueResolveRow { id, label }))
88    }
89
90    /// Insert a validated attribute-value row. Unique-constraint errors propagate as
91    /// `sqlx::Error` so the service can disambiguate code duplicates.
92    pub async fn insert_attribute_value(
93        &self,
94        executor: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
95        r: &NewAttributeValueRow<'_>,
96    ) -> Result<(), sqlx::Error> {
97        sqlx::query(
98            r#"INSERT INTO catalog.attribute_values
99                (id, attribute_id, code, label, label_en, swatch_hex, sort_order, status)
100               VALUES ($1,$2,$3,$4,$5,$6,$7,'active'::catalog_status)"#,
101        )
102        .bind(r.id)
103        .bind(r.attribute_id)
104        .bind(r.code)
105        .bind(r.label)
106        .bind(r.label_en)
107        .bind(r.swatch_hex)
108        .bind(r.sort_order)
109        .execute(executor)
110        .await?;
111        Ok(())
112    }
113}
114
115backbone_core::impl_crud_repository!(AttributeValueRepository, AttributeValue, soft_delete);