coil-data 0.1.1

Data access and persistence primitives for the Coil framework.
Documentation
use crate::{
    CompiledQuery, DataModelError, FilterOperator, QueryField, QueryFilter, QuerySort, QuerySpec,
    TableName, compile_filters, ensure_repository_field, quote_identifier, require_non_empty,
};

#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct RepositoryContextBindings {
    pub locale_field: Option<QueryField>,
    pub publication_field: Option<QueryField>,
    pub published_value: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RepositorySpec {
    pub id: String,
    pub table: TableName,
    pub projection: Vec<QueryField>,
    pub filterable_fields: Vec<QueryField>,
    pub sortable_fields: Vec<QueryField>,
    pub default_sort: Vec<QuerySort>,
    pub context: RepositoryContextBindings,
}

impl RepositorySpec {
    pub fn new(
        id: impl Into<String>,
        table: TableName,
        projection: Vec<QueryField>,
    ) -> Result<Self, DataModelError> {
        let id = require_non_empty("repository_id", id.into())?;
        if projection.is_empty() {
            return Err(DataModelError::EmptyProjection { repository: id });
        }

        Ok(Self {
            id,
            table,
            filterable_fields: projection.clone(),
            sortable_fields: projection.clone(),
            projection,
            default_sort: Vec::new(),
            context: RepositoryContextBindings::default(),
        })
    }

    pub fn with_filterable_field(
        mut self,
        field: impl Into<String>,
    ) -> Result<Self, DataModelError> {
        let field = QueryField::new(field)?;
        if !self.filterable_fields.contains(&field) {
            self.filterable_fields.push(field);
        }
        Ok(self)
    }

    pub fn with_sortable_field(mut self, field: impl Into<String>) -> Result<Self, DataModelError> {
        let field = QueryField::new(field)?;
        if !self.sortable_fields.contains(&field) {
            self.sortable_fields.push(field);
        }
        Ok(self)
    }

    pub fn with_default_sort(mut self, sort: QuerySort) -> Self {
        self.default_sort.push(sort);
        self
    }

    pub fn with_locale_field(mut self, field: impl Into<String>) -> Result<Self, DataModelError> {
        self.context.locale_field = Some(QueryField::new(field)?);
        Ok(self)
    }

    pub fn with_publication_field(
        mut self,
        field: impl Into<String>,
        published_value: impl Into<String>,
    ) -> Result<Self, DataModelError> {
        self.context.publication_field = Some(QueryField::new(field)?);
        self.context.published_value = Some(require_non_empty(
            "published_value",
            published_value.into(),
        )?);
        Ok(self)
    }

    pub fn compile_query(&self, spec: &QuerySpec) -> Result<CompiledQuery, DataModelError> {
        let mut filters = Vec::new();
        if let (Some(locale), Some(locale_field)) = (
            spec.context.locale.as_ref(),
            self.context.locale_field.as_ref(),
        ) {
            filters.push(QueryFilter::new(
                locale_field.as_str(),
                FilterOperator::Eq,
                vec![locale.clone()],
            )?);
        }

        if let (
            crate::PublicationVisibility::PublishedOnly,
            Some(publication_field),
            Some(published),
        ) = (
            spec.context.publication_visibility,
            self.context.publication_field.as_ref(),
            self.context.published_value.as_ref(),
        ) {
            filters.push(QueryFilter::new(
                publication_field.as_str(),
                FilterOperator::Eq,
                vec![published.clone()],
            )?);
        }

        filters.extend(spec.filters.clone());

        for filter in &filters {
            ensure_repository_field(
                &self.id,
                &filter.field,
                &self.filterable_fields,
                self.context.locale_field.as_ref(),
                self.context.publication_field.as_ref(),
            )?;
        }

        let sort = if spec.sort.is_empty() {
            self.default_sort.clone()
        } else {
            spec.sort.clone()
        };

        for sort_field in &sort {
            ensure_repository_field(
                &self.id,
                &sort_field.field,
                &self.sortable_fields,
                self.context.locale_field.as_ref(),
                self.context.publication_field.as_ref(),
            )?;
        }

        let projection = self
            .projection
            .iter()
            .map(|field| quote_identifier(field.as_str()))
            .collect::<Vec<_>>()
            .join(", ");
        let mut sql = format!(
            "SELECT {projection} FROM {}",
            quote_identifier(self.table.as_str())
        );

        let (where_clauses, bind_values, _) = compile_filters(&filters, 1)?;
        if !where_clauses.is_empty() {
            sql.push_str(" WHERE ");
            sql.push_str(&where_clauses.join(" AND "));
        }

        if !sort.is_empty() {
            sql.push_str(" ORDER BY ");
            sql.push_str(
                &sort
                    .iter()
                    .map(|sort| {
                        format!(
                            "{} {}",
                            quote_identifier(sort.field.as_str()),
                            sort.direction.to_string().to_uppercase()
                        )
                    })
                    .collect::<Vec<_>>()
                    .join(", "),
            );
        }

        sql.push_str(&format!(
            " LIMIT {} OFFSET {}",
            spec.page.size,
            spec.page.offset()
        ));

        Ok(CompiledQuery {
            sql,
            bind_values,
            page: spec.page,
            context: spec.context.clone(),
        })
    }
}