Skip to main content

alibi_sqlx/
organization_models.rs

1//! Application-owned organization plugin tables. Models derive [`crate::SqlxModel`]
2//! and use Rust names for plugin fields, with `sqlx(rename)` for physical names.
3
4use crate::model::{self, ActiveRow, SqlxModel};
5use crate::pool::Exec;
6use crate::sql::Sql;
7use crate::store::entities::{invitation, member, organization};
8use crate::value::{ColumnKind, SqlValue};
9use alibi_core::{AuthError, AuthResult};
10use async_trait::async_trait;
11use std::{marker::PhantomData, sync::Arc};
12
13/// Bind the three organization entities to application models and ID factories.
14/// This affects every plugin operation, including invitation transactions.
15#[derive(Clone)]
16pub struct OrganizationModels {
17    pub(crate) organization: Binding<organization::Model>,
18    pub(crate) member: Binding<member::Model>,
19    pub(crate) invitation: Binding<invitation::Model>,
20}
21
22impl OrganizationModels {
23    /// Models must declare the plugin's Rust field names. `updated_at` on the
24    /// organization and `team_id` on the invitation may be omitted. Additional
25    /// organization fields are retained and returned by the plugin.
26    #[must_use]
27    pub fn new<O: SqlxModel, M: SqlxModel, I: SqlxModel>(
28        organization_id: fn() -> String,
29        member_id: fn() -> String,
30        invitation_id: fn() -> String,
31    ) -> Self {
32        Self {
33            organization: Binding::new::<O>(organization_id),
34            member: Binding::new::<M>(member_id),
35            invitation: Binding::new::<I>(invitation_id),
36        }
37    }
38}
39
40impl Default for OrganizationModels {
41    fn default() -> Self {
42        fn id() -> String {
43            uuid::Uuid::new_v4().to_string()
44        }
45        Self::new::<organization::Model, member::Model, invitation::Model>(id, id, id)
46    }
47}
48
49#[async_trait]
50trait Backend: Send + Sync {
51    fn table(&self) -> &'static str;
52    fn columns(&self) -> &'static [&'static str];
53    fn fields(&self) -> &'static [(&'static str, &'static str)];
54    fn kind(&self, physical: &str) -> ColumnKind;
55    async fn fetch(&self, exec: Exec<'_>, sql: Sql) -> AuthResult<Vec<ActiveRow>>;
56    async fn insert(&self, exec: Exec<'_>, row: &ActiveRow) -> AuthResult<ActiveRow>;
57    async fn update(&self, exec: Exec<'_>, row: &ActiveRow) -> AuthResult<Option<ActiveRow>>;
58}
59struct TypedBackend<M>(PhantomData<M>);
60#[async_trait]
61impl<M: SqlxModel> Backend for TypedBackend<M> {
62    fn table(&self) -> &'static str {
63        M::TABLE
64    }
65    fn columns(&self) -> &'static [&'static str] {
66        M::COLUMN_NAMES
67    }
68    fn fields(&self) -> &'static [(&'static str, &'static str)] {
69        M::FIELD_COLUMNS
70    }
71    fn kind(&self, physical: &str) -> ColumnKind {
72        M::column_kind(physical)
73    }
74    async fn fetch(&self, exec: Exec<'_>, sql: Sql) -> AuthResult<Vec<ActiveRow>> {
75        Ok(exec
76            .fetch_all::<M>(sql)
77            .await?
78            .into_iter()
79            .map(SqlxModel::into_active)
80            .collect())
81    }
82    async fn insert(&self, exec: Exec<'_>, row: &ActiveRow) -> AuthResult<ActiveRow> {
83        Ok(model::insert::<M>(exec, row).await?.into_active())
84    }
85    async fn update(&self, exec: Exec<'_>, row: &ActiveRow) -> AuthResult<Option<ActiveRow>> {
86        Ok(model::update::<M>(exec, row)
87            .await?
88            .map(SqlxModel::into_active))
89    }
90}
91
92#[derive(Clone)]
93pub(crate) struct Binding<T> {
94    backend: Arc<dyn Backend>,
95    generate_id: fn() -> String,
96    _canonical: PhantomData<T>,
97}
98
99/// Canonical plugin fields and the original loaded write state. Keeping that
100/// state prevents an update from overwriting unrelated application columns.
101pub(crate) struct Row<T> {
102    value: T,
103    active: ActiveRow,
104}
105impl<T> std::ops::Deref for Row<T> {
106    type Target = T;
107    fn deref(&self) -> &T {
108        &self.value
109    }
110}
111impl<T> Row<T> {
112    pub(crate) fn active_role(&self) -> SqlValue {
113        self.active
114            .get("role")
115            .and_then(crate::ActiveValue::value)
116            .cloned()
117            .unwrap_or(SqlValue::Text(None))
118    }
119    pub(crate) fn into_active(self) -> ActiveRow {
120        self.active
121    }
122}
123
124impl<T: SqlxModel> Binding<T> {
125    fn new<M: SqlxModel>(generate_id: fn() -> String) -> Self {
126        Self {
127            backend: Arc::new(TypedBackend::<M>(PhantomData)),
128            generate_id,
129            _canonical: PhantomData,
130        }
131    }
132    pub(crate) async fn set_fields(
133        &self,
134        exec: Exec<'_>,
135        active: &mut ActiveRow,
136        fields: &mut alibi_core::field_policy::FieldValues,
137    ) -> AuthResult<()> {
138        fields.apply_adapter_transforms_async().await?;
139        for (name, value) in &*fields {
140            let requested = fields.binding_name(name);
141            let column = self.physical(requested).ok_or_else(|| {
142                AuthError::config(format!(
143                    "configured organization field {name} has no model column"
144                ))
145            })?;
146            let field = self.logical(column);
147            if T::COLUMN_NAMES.contains(&field) {
148                return Err(AuthError::config(
149                    "additional organization fields cannot overwrite plugin fields",
150                ));
151            }
152            let value = crate::additional_fields::prepare_value(
153                exec,
154                self.backend.kind(column),
155                crate::additional_fields::raw_value(value)?,
156            )
157            .await?;
158            active.set(field, value);
159        }
160        Ok(())
161    }
162    pub(crate) fn new_id(&self) -> String {
163        (self.generate_id)()
164    }
165    pub(crate) fn table(&self) -> &'static str {
166        self.backend.table()
167    }
168    fn physical(&self, field: &str) -> Option<&'static str> {
169        self.backend
170            .fields()
171            .iter()
172            .find(|(logical, _)| *logical == field)
173            .map(|(_, physical)| *physical)
174            .or_else(|| {
175                self.backend
176                    .columns()
177                    .iter()
178                    .copied()
179                    .find(|column| *column == field)
180            })
181    }
182    fn logical(&self, physical: &'static str) -> &'static str {
183        self.backend
184            .fields()
185            .iter()
186            .find(|(_, column)| *column == physical)
187            .map_or(physical, |(logical, _)| *logical)
188    }
189    fn value(&self, field: &str, value: SqlValue) -> AuthResult<SqlValue> {
190        let column = self.physical(field).ok_or_else(|| {
191            AuthError::internal(format!("organization model has no {field} column"))
192        })?;
193        Ok(match (self.backend.kind(column), value) {
194            (ColumnKind::BpChar, SqlValue::Text(value)) => SqlValue::BpChar(value),
195            (ColumnKind::NaiveTimestamp, SqlValue::Timestamp(value)) => {
196                SqlValue::NaiveTimestamp(value.map(|time| time.naive_utc()))
197            }
198            (ColumnKind::Text, SqlValue::Json(value)) => SqlValue::Text(
199                value
200                    .map(|value| alibi_core::utils::json::to_string(&*value))
201                    .transpose()?,
202            ),
203            (_, value) => value,
204        })
205    }
206    fn stage(&self, active: &ActiveRow) -> AuthResult<ActiveRow> {
207        let mut physical = ActiveRow::new();
208        for (field, value) in active.present() {
209            let Some(column) = self.physical(field) else {
210                if field == "updated_at" || (field == "team_id" && value.is_null()) {
211                    continue;
212                }
213                return Err(AuthError::internal(format!(
214                    "organization model has no {field} column"
215                )));
216            };
217            let value = self.value(field, value.clone())?;
218            if active.get(field).is_some_and(crate::ActiveValue::is_set) {
219                physical.set(column, value);
220            } else {
221                physical.unchanged(column, value);
222            }
223        }
224        Ok(physical)
225    }
226    fn row(&self, physical: ActiveRow) -> AuthResult<Row<T>> {
227        let mut active = ActiveRow::new();
228        for (column, value) in physical.present() {
229            let field = self.logical(column);
230            let value = match value.clone() {
231                SqlValue::NaiveTimestamp(value) => {
232                    SqlValue::Timestamp(value.map(|time| time.and_utc()))
233                }
234                SqlValue::BpChar(value) => SqlValue::Text(value),
235                value => value,
236            };
237            active.unchanged(field, value);
238        }
239        let mut canonical = active.clone();
240        if T::TABLE == organization::Model::TABLE {
241            if self.physical("updated_at").is_none()
242                && let Some(time) = active.get("created_at").and_then(crate::ActiveValue::value)
243            {
244                canonical.unchanged("updated_at", time.clone());
245            }
246            if canonical
247                .get("metadata")
248                .and_then(crate::ActiveValue::value)
249                .is_some_and(SqlValue::is_null)
250            {
251                canonical.unchanged("metadata", SqlValue::Json(None));
252            }
253        }
254        if T::TABLE == invitation::Model::TABLE {
255            if self.physical("team_id").is_none() {
256                canonical.unchanged("team_id", None::<String>);
257            }
258            // The canonical storage row uses a String. The original nullable
259            // value remains in the loaded row for its public representation.
260            if canonical
261                .get("role")
262                .and_then(crate::ActiveValue::value)
263                .is_some_and(SqlValue::is_null)
264            {
265                canonical.unchanged("role", "");
266            }
267        }
268        Ok(Row {
269            value: T::from_active(canonical)?,
270            active,
271        })
272    }
273    pub(crate) fn select(&self, exec: Exec<'_>) -> Sql {
274        crate::sql::select(exec.engine(), self.table(), self.backend.columns())
275    }
276    pub(crate) fn by_id(&self, exec: Exec<'_>, id: impl Into<SqlValue>) -> AuthResult<Sql> {
277        let mut sql = self.select(exec);
278        sql.push(" WHERE ");
279        self.compare(&mut sql, "id", " = ", id)?;
280        Ok(sql)
281    }
282    pub(crate) fn delete_for_organization(&self, exec: Exec<'_>, id: &str) -> AuthResult<Sql> {
283        let mut sql = Sql::with(exec.engine(), "DELETE FROM ");
284        sql.ident(self.table());
285        sql.push(" WHERE ");
286        self.compare(&mut sql, "organization_id", " = ", id)?;
287        Ok(sql)
288    }
289    pub(crate) fn delete_by_id(&self, exec: Exec<'_>, id: impl Into<SqlValue>) -> AuthResult<Sql> {
290        let mut sql = Sql::with(exec.engine(), "DELETE FROM ");
291        sql.ident(self.table());
292        sql.push(" WHERE ");
293        self.compare(&mut sql, "id", " = ", id)?;
294        Ok(sql)
295    }
296    pub(crate) fn column(&self, sql: &mut Sql, field: &str) -> AuthResult<()> {
297        let column = self.physical(field).ok_or_else(|| {
298            AuthError::internal(format!("organization model has no {field} column"))
299        })?;
300        sql.column(self.table(), column);
301        Ok(())
302    }
303    pub(crate) fn compare(
304        &self,
305        sql: &mut Sql,
306        field: &str,
307        operator: &str,
308        value: impl Into<SqlValue>,
309    ) -> AuthResult<()> {
310        self.column(sql, field)?;
311        sql.push(operator);
312        sql.bind(self.value(field, value.into())?);
313        Ok(())
314    }
315    pub(crate) fn assign(
316        &self,
317        sql: &mut Sql,
318        field: &str,
319        value: impl Into<SqlValue>,
320    ) -> AuthResult<()> {
321        let column = self.physical(field).ok_or_else(|| {
322            AuthError::internal(format!("organization model has no {field} column"))
323        })?;
324        sql.assign(column, self.value(field, value.into())?);
325        Ok(())
326    }
327    pub(crate) fn bind_list<I: IntoIterator<Item = String>>(
328        &self,
329        sql: &mut Sql,
330        field: &str,
331        values: I,
332    ) -> AuthResult<()> {
333        sql.bind_list(
334            values
335                .into_iter()
336                .map(|value| self.value(field, value.into()))
337                .collect::<AuthResult<Vec<_>>>()?,
338        );
339        Ok(())
340    }
341    pub(crate) fn returning(&self, sql: &mut Sql) {
342        sql.push(" RETURNING ");
343        sql.column_list(self.backend.columns());
344    }
345    pub(crate) async fn fetch_all(&self, exec: Exec<'_>, sql: Sql) -> AuthResult<Vec<Row<T>>> {
346        self.backend
347            .fetch(exec, sql)
348            .await?
349            .into_iter()
350            .map(|active| self.row(active))
351            .collect()
352    }
353    pub(crate) async fn fetch_optional(
354        &self,
355        exec: Exec<'_>,
356        sql: Sql,
357    ) -> AuthResult<Option<Row<T>>> {
358        Ok(self.fetch_all(exec, sql).await?.into_iter().next())
359    }
360    pub(crate) async fn insert(&self, exec: Exec<'_>, active: &ActiveRow) -> AuthResult<Row<T>> {
361        self.row(self.backend.insert(exec, &self.stage(active)?).await?)
362    }
363    pub(crate) async fn update(
364        &self,
365        exec: Exec<'_>,
366        active: &ActiveRow,
367    ) -> AuthResult<Option<Row<T>>> {
368        self.backend
369            .update(exec, &self.stage(active)?)
370            .await?
371            .map(|active| self.row(active))
372            .transpose()
373    }
374}
375
376impl From<&Row<organization::Model>> for alibi_core::Organization {
377    fn from(row: &Row<organization::Model>) -> Self {
378        let mut organization = Self::from(&row.value);
379        for (field, value) in row.active.present() {
380            if organization::Model::COLUMN_NAMES.contains(&field) {
381                continue;
382            }
383            if let Some(value) = output_value(value) {
384                _ = organization
385                    .additional_fields
386                    .insert(field.to_owned(), value);
387            }
388        }
389        organization
390    }
391}
392impl From<&Row<member::Model>> for alibi_core::Member {
393    fn from(row: &Row<member::Model>) -> Self {
394        Self::from(&row.value)
395    }
396}
397impl From<&Row<invitation::Model>> for alibi_core::Invitation {
398    fn from(row: &Row<invitation::Model>) -> Self {
399        let mut invitation = Self::from(&row.value);
400        if row
401            .active
402            .get("role")
403            .and_then(crate::ActiveValue::value)
404            .is_some_and(SqlValue::is_null)
405        {
406            invitation.role = None;
407        }
408        invitation
409    }
410}
411
412fn output_value(value: &SqlValue) -> Option<serde_json::Value> {
413    if value.is_null() {
414        return Some(serde_json::Value::Null);
415    }
416    match value {
417        SqlValue::Text(Some(value)) | SqlValue::BpChar(Some(value)) => Some(value.clone().into()),
418        SqlValue::Bool(Some(value)) => Some((*value).into()),
419        SqlValue::Int(Some(value)) => Some((*value).into()),
420        SqlValue::BigInt(Some(value)) => Some((*value).into()),
421        SqlValue::Float(Some(value)) => {
422            serde_json::Number::from_f64(f64::from(*value)).map(Into::into)
423        }
424        SqlValue::Double(Some(value)) => serde_json::Number::from_f64(*value).map(Into::into),
425        SqlValue::Json(Some(value)) => Some((**value).clone()),
426        _ => None,
427    }
428}