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