backbone-catalog 0.7.0

Canonical product/service identity: Item, Item Group, UOM (Indonesia-first)
Documentation
//! Repository for AttributeValue entities
//!
//! Originally generated by metaphor-schema; now **user-owned** — this exact path is declared under
//! `user_owned` in `metaphor.codegen.yaml`, so the generator skips it wholesale. The custom methods
//! below hold the catalog write service's attribute-value SQL (4-layer rule: services orchestrate,
//! repos hold SQL).
//!
//! Tenant-agnostic (ADR-0029): no statement here names a tenancy column. When the composing
//! host mounts a request-scoped org fence, plain pool reads ride the request-dedicated
//! scoped connection and see only in-scope rows; undecorated, they see the whole table.
//!
//! Thin newtype over `backbone_orm::GenericCrudRepository<AttributeValue, backbone_orm::SoftDelete>`.
//! All standard CRUD methods are available via `Deref`.

use sqlx::PgPool;
use uuid::Uuid;

use crate::domain::entity::AttributeValue;

/// Table name for AttributeValue entities
pub const TABLE_NAME: &str = "catalog.attribute_values";

/// Repository for AttributeValue entities.
///
/// All standard CRUD, soft-delete, pagination, and bulk methods are
/// provided automatically via `Deref` to `backbone_orm::GenericCrudRepository`.
pub struct AttributeValueRepository(
    backbone_orm::GenericCrudRepository<AttributeValue, backbone_orm::SoftDelete>,
);

impl std::ops::Deref for AttributeValueRepository {
    type Target = backbone_orm::GenericCrudRepository<AttributeValue, backbone_orm::SoftDelete>;
    fn deref(&self) -> &Self::Target { &self.0 }
}

impl AttributeValueRepository {
    /// Create a new repository instance.
    pub fn new(pool: PgPool) -> Self {
        Self(backbone_orm::GenericCrudRepository::new(pool, TABLE_NAME))
    }
}

/// The exact row a validated attribute-value insert writes.
pub struct NewAttributeValueRow<'a> {
    pub id: Uuid,
    pub attribute_id: Uuid,
    pub code: &'a str,
    pub label: &'a str,
    pub label_en: Option<&'a str>,
    pub swatch_hex: Option<&'a str>,
    pub sort_order: i32,
}

/// A resolved `(value_id, label)` pair from the attribute_value × attribute join (used by
/// create-item-variant to validate each option against the registry and to build the variant's
/// default label).
#[derive(Debug, Clone)]
pub struct AttributeValueResolveRow {
    pub id: Uuid,
    pub label: String,
}

/// Catalog attribute-value SQL. Lives here (not in the service) per the module's 4-layer rule.
impl AttributeValueRepository {
    /// Resolve `(value_id, label)` for a `(attribute_code, value_code)` pair, joined with the
    /// attribute registry. Tenant-agnostic: scoped by the caller's connection, never by a
    /// column here. `None` = the value or its axis is unknown; the caller disambiguates with
    /// [`AttributeRepository::find_id_by_code`].
    pub async fn find_value_with_attribute(
        &self,
        executor: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
        attr_code: &str,
        val_code: &str,
    ) -> Result<Option<AttributeValueResolveRow>, sqlx::Error> {
        let row: Option<(Uuid, String)> = sqlx::query_as(
            r#"SELECT av.id, av.label
               FROM catalog.attribute_values av
               JOIN catalog.attributes a ON a.id = av.attribute_id
               WHERE a.code = $1 AND av.code = $2
                 AND (a.metadata->>'deleted_at') IS NULL
                 AND (av.metadata->>'deleted_at') IS NULL"#,
        )
        .bind(attr_code)
        .bind(val_code)
        .fetch_optional(executor)
        .await?;
        Ok(row.map(|(id, label)| AttributeValueResolveRow { id, label }))
    }

    /// Insert a validated attribute-value row. Unique-constraint errors propagate as
    /// `sqlx::Error` so the service can disambiguate code duplicates.
    pub async fn insert_attribute_value(
        &self,
        executor: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
        r: &NewAttributeValueRow<'_>,
    ) -> Result<(), sqlx::Error> {
        sqlx::query(
            r#"INSERT INTO catalog.attribute_values
                (id, attribute_id, code, label, label_en, swatch_hex, sort_order, status)
               VALUES ($1,$2,$3,$4,$5,$6,$7,'active'::catalog_status)"#,
        )
        .bind(r.id)
        .bind(r.attribute_id)
        .bind(r.code)
        .bind(r.label)
        .bind(r.label_en)
        .bind(r.swatch_hex)
        .bind(r.sort_order)
        .execute(executor)
        .await?;
        Ok(())
    }
}

backbone_core::impl_crud_repository!(AttributeValueRepository, AttributeValue, soft_delete);