Skip to main content

coil_data/
repository.rs

1use crate::{
2    CompiledQuery, DataModelError, FilterOperator, QueryField, QueryFilter, QuerySort, QuerySpec,
3    TableName, compile_filters, ensure_repository_field, quote_identifier, require_non_empty,
4};
5
6#[derive(Debug, Clone, PartialEq, Eq, Default)]
7pub struct RepositoryContextBindings {
8    pub locale_field: Option<QueryField>,
9    pub publication_field: Option<QueryField>,
10    pub published_value: Option<String>,
11}
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct RepositorySpec {
15    pub id: String,
16    pub table: TableName,
17    pub projection: Vec<QueryField>,
18    pub filterable_fields: Vec<QueryField>,
19    pub sortable_fields: Vec<QueryField>,
20    pub default_sort: Vec<QuerySort>,
21    pub context: RepositoryContextBindings,
22}
23
24impl RepositorySpec {
25    pub fn new(
26        id: impl Into<String>,
27        table: TableName,
28        projection: Vec<QueryField>,
29    ) -> Result<Self, DataModelError> {
30        let id = require_non_empty("repository_id", id.into())?;
31        if projection.is_empty() {
32            return Err(DataModelError::EmptyProjection { repository: id });
33        }
34
35        Ok(Self {
36            id,
37            table,
38            filterable_fields: projection.clone(),
39            sortable_fields: projection.clone(),
40            projection,
41            default_sort: Vec::new(),
42            context: RepositoryContextBindings::default(),
43        })
44    }
45
46    pub fn with_filterable_field(
47        mut self,
48        field: impl Into<String>,
49    ) -> Result<Self, DataModelError> {
50        let field = QueryField::new(field)?;
51        if !self.filterable_fields.contains(&field) {
52            self.filterable_fields.push(field);
53        }
54        Ok(self)
55    }
56
57    pub fn with_sortable_field(mut self, field: impl Into<String>) -> Result<Self, DataModelError> {
58        let field = QueryField::new(field)?;
59        if !self.sortable_fields.contains(&field) {
60            self.sortable_fields.push(field);
61        }
62        Ok(self)
63    }
64
65    pub fn with_default_sort(mut self, sort: QuerySort) -> Self {
66        self.default_sort.push(sort);
67        self
68    }
69
70    pub fn with_locale_field(mut self, field: impl Into<String>) -> Result<Self, DataModelError> {
71        self.context.locale_field = Some(QueryField::new(field)?);
72        Ok(self)
73    }
74
75    pub fn with_publication_field(
76        mut self,
77        field: impl Into<String>,
78        published_value: impl Into<String>,
79    ) -> Result<Self, DataModelError> {
80        self.context.publication_field = Some(QueryField::new(field)?);
81        self.context.published_value = Some(require_non_empty(
82            "published_value",
83            published_value.into(),
84        )?);
85        Ok(self)
86    }
87
88    pub fn compile_query(&self, spec: &QuerySpec) -> Result<CompiledQuery, DataModelError> {
89        let mut filters = Vec::new();
90        if let (Some(locale), Some(locale_field)) = (
91            spec.context.locale.as_ref(),
92            self.context.locale_field.as_ref(),
93        ) {
94            filters.push(QueryFilter::new(
95                locale_field.as_str(),
96                FilterOperator::Eq,
97                vec![locale.clone()],
98            )?);
99        }
100
101        if let (
102            crate::PublicationVisibility::PublishedOnly,
103            Some(publication_field),
104            Some(published),
105        ) = (
106            spec.context.publication_visibility,
107            self.context.publication_field.as_ref(),
108            self.context.published_value.as_ref(),
109        ) {
110            filters.push(QueryFilter::new(
111                publication_field.as_str(),
112                FilterOperator::Eq,
113                vec![published.clone()],
114            )?);
115        }
116
117        filters.extend(spec.filters.clone());
118
119        for filter in &filters {
120            ensure_repository_field(
121                &self.id,
122                &filter.field,
123                &self.filterable_fields,
124                self.context.locale_field.as_ref(),
125                self.context.publication_field.as_ref(),
126            )?;
127        }
128
129        let sort = if spec.sort.is_empty() {
130            self.default_sort.clone()
131        } else {
132            spec.sort.clone()
133        };
134
135        for sort_field in &sort {
136            ensure_repository_field(
137                &self.id,
138                &sort_field.field,
139                &self.sortable_fields,
140                self.context.locale_field.as_ref(),
141                self.context.publication_field.as_ref(),
142            )?;
143        }
144
145        let projection = self
146            .projection
147            .iter()
148            .map(|field| quote_identifier(field.as_str()))
149            .collect::<Vec<_>>()
150            .join(", ");
151        let mut sql = format!(
152            "SELECT {projection} FROM {}",
153            quote_identifier(self.table.as_str())
154        );
155
156        let (where_clauses, bind_values, _) = compile_filters(&filters, 1)?;
157        if !where_clauses.is_empty() {
158            sql.push_str(" WHERE ");
159            sql.push_str(&where_clauses.join(" AND "));
160        }
161
162        if !sort.is_empty() {
163            sql.push_str(" ORDER BY ");
164            sql.push_str(
165                &sort
166                    .iter()
167                    .map(|sort| {
168                        format!(
169                            "{} {}",
170                            quote_identifier(sort.field.as_str()),
171                            sort.direction.to_string().to_uppercase()
172                        )
173                    })
174                    .collect::<Vec<_>>()
175                    .join(", "),
176            );
177        }
178
179        sql.push_str(&format!(
180            " LIMIT {} OFFSET {}",
181            spec.page.size,
182            spec.page.offset()
183        ));
184
185        Ok(CompiledQuery {
186            sql,
187            bind_values,
188            page: spec.page,
189            context: spec.context.clone(),
190        })
191    }
192}